@quantiya/codevibe-claude-plugin 1.0.44 → 1.0.46

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.
Files changed (34) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/server.js +8 -8
  3. package/node_modules/@quantiya/codevibe-core/dist/index.js +21 -21
  4. package/node_modules/@quantiya/codevibe-core/package.json +1 -1
  5. package/node_modules/es-object-atoms/CHANGELOG.md +21 -14
  6. package/node_modules/es-object-atoms/isObject.d.ts +1 -1
  7. package/node_modules/es-object-atoms/package.json +6 -7
  8. package/node_modules/es-object-atoms/tsconfig.json +1 -0
  9. package/node_modules/hasown/CHANGELOG.md +7 -0
  10. package/node_modules/hasown/index.d.ts +0 -1
  11. package/node_modules/hasown/package.json +4 -5
  12. package/node_modules/qs/CHANGELOG.md +10 -0
  13. package/node_modules/qs/README.md +1 -1
  14. package/node_modules/qs/dist/qs.js +15 -15
  15. package/node_modules/qs/eslint.config.mjs +1 -0
  16. package/node_modules/qs/lib/parse.js +52 -22
  17. package/node_modules/qs/lib/stringify.js +11 -4
  18. package/node_modules/qs/package.json +2 -2
  19. package/node_modules/qs/test/parse.js +49 -0
  20. package/node_modules/qs/test/stringify.js +129 -0
  21. package/node_modules/type-is/index.js +8 -18
  22. package/node_modules/type-is/node_modules/content-type/LICENSE +22 -0
  23. package/node_modules/type-is/node_modules/content-type/README.md +69 -0
  24. package/node_modules/type-is/node_modules/content-type/dist/index.d.ts +26 -0
  25. package/node_modules/type-is/node_modules/content-type/dist/index.js +170 -0
  26. package/node_modules/type-is/node_modules/content-type/dist/index.js.map +1 -0
  27. package/node_modules/type-is/node_modules/content-type/package.json +52 -0
  28. package/node_modules/type-is/package.json +9 -5
  29. package/node_modules/ws/lib/receiver.js +54 -0
  30. package/node_modules/ws/lib/sender.js +6 -1
  31. package/node_modules/ws/lib/websocket-server.js +8 -0
  32. package/node_modules/ws/lib/websocket.js +14 -0
  33. package/node_modules/ws/package.json +1 -1
  34. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevibe-claude",
3
- "version": "1.0.44",
3
+ "version": "1.0.46",
4
4
  "description": "Sync Claude Code sessions with iOS mobile app via AWS backend. Control Claude Code from your phone with real-time bidirectional synchronization.",
5
5
  "author": {
6
6
  "name": "CodeVibe Team"
package/dist/server.js CHANGED
@@ -1,17 +1,17 @@
1
- "use strict";var ke=Object.create;var X=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Ie=Object.getOwnPropertyNames;var Ce=Object.getPrototypeOf,Ae=Object.prototype.hasOwnProperty;var Te=(w,e)=>{for(var t in e)X(w,t,{get:e[t],enumerable:!0})},ce=(w,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of Ie(e))!Ae.call(w,n)&&n!==t&&X(w,n,{get:()=>e[n],enumerable:!(r=be(e,n))||r.enumerable});return w};var O=(w,e,t)=>(t=w!=null?ke(Ce(w)):{},ce(e||!w||!w.__esModule?X(t,"default",{value:w,enumerable:!0}):t,w)),xe=w=>ce(X({},"__esModule",{value:!0}),w);var Oe={};Te(Oe,{McpServer:()=>te,parseInteractivePromptInput:()=>Pe});module.exports=xe(Oe);var D=O(require("fs")),V=O(require("path")),ee=O(require("os")),ve=require("child_process"),Ee=require("util"),Se=require("child_process"),W=require("crypto");var de=O(require("os")),le=O(require("path")),ue=require("@quantiya/codevibe-core"),i=(0,ue.createLogger)({name:"codevibe-claude",logFile:le.default.join(de.default.tmpdir(),"codevibe-claude-mcp.log"),level:"info"});var p=require("@quantiya/codevibe-core");var ne=O(require("express")),B=O(require("fs")),re=O(require("path")),ie=O(require("os")),me=require("@quantiya/codevibe-core");var S=require("@quantiya/codevibe-core");var z=class{constructor(){this.assignedPort=0;this.app=(0,ne.default)(),this.setupMiddleware(),this.setupRoutes()}setSessionId(e){this.sessionId=e}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(ne.default.json({limit:"1mb"})),this.app.use((e,t,r)=>{i.debug(`${e.method} ${e.path}`,{body:e.body,query:e.query}),r()}),this.app.use((e,t,r,n)=>{i.error("Express error:",e);let s={success:!1,error:e.message||"Internal server error"};r.status(500).json(s)})}setupRoutes(){this.app.get("/health",this.handleHealth.bind(this)),this.app.post("/event",this.handleEvent.bind(this)),process.env.NODE_ENV!=="production"&&this.app.post("/test/execute",this.handleTestExecute.bind(this))}handleHealth(e,t){let r={success:!0,data:{status:"healthy",uptime:process.uptime(),version:"0.1.0",timestamp:new Date().toISOString()}};t.json(r)}async handleEvent(e,t){try{let r=e.body;if(!r.session_id){let o={success:!1,error:"Missing required field: session_id"};t.status(400).json(o);return}if(!r.hook_event_name){let o={success:!1,error:"Missing required field: hook_event_name"};t.status(400).json(o);return}let n=this.transformHookToEvent(r);i.info("Received event from hook",{sessionId:r.session_id,hookEvent:r.hook_event_name,type:n.type}),this.eventHandler?await this.eventHandler(n):i.warn("No event handler registered");let s={success:!0,message:"Event processed successfully"};t.json(s)}catch(r){i.error("Error handling event:",r);let n={success:!1,error:r instanceof Error?r.message:"Unknown error"};t.status(500).json(n)}}async handleTestExecute(e,t){try{let{sessionId:r,prompt:n}=e.body;if(!r||!n){let o={success:!1,error:"Missing required fields: sessionId, prompt"};t.status(400).json(o);return}i.info("Test execute request",{sessionId:r,prompt:n});let s={success:!0,message:"Test execution endpoint - not implemented yet",data:{sessionId:r,prompt:n}};t.json(s)}catch(r){i.error("Error in test execute:",r);let n={success:!1,error:r instanceof Error?r.message:"Unknown error"};t.status(500).json(n)}}transformHookToEvent(e){let t,r,n={cwd:e.cwd,hook_event_name:e.hook_event_name,...e.metadata||{}};if(e.type&&e.content!==void 0)t=e.type,r=e.content;else switch(e.hook_event_name){case"SessionStart":t=S.EventType.NOTIFICATION,r="Session started",n.source=e.source;break;case"SessionEnd":t=S.EventType.NOTIFICATION,r=`Session ended: ${e.reason||"unknown"}`,n.reason=e.reason;break;case"UserPromptSubmit":t=S.EventType.USER_PROMPT,r=e.prompt||"";break;case"PostToolUse":t=S.EventType.TOOL_USE,r=JSON.stringify({tool_name:e.tool_name,tool_input:e.tool_input,tool_response:e.tool_response}),n.tool_name=e.tool_name;break;case"Notification":t=S.EventType.NOTIFICATION,r=e.message||"",n.notification_type=e.notification_type;break;default:t=S.EventType.NOTIFICATION,r=`Hook event: ${e.hook_event_name}`}return{session_id:e.session_id,hook_event_name:e.hook_event_name,type:t,source:S.EventSource.DESKTOP,content:r,metadata:n}}onEvent(e){this.eventHandler=e}async start(e){let t=e||this.sessionId;return t&&(this.sessionId=t),new Promise((r,n)=>{try{let s=(0,me.getConfig)(),o=s.server.dynamicPort?0:s.server.port;this.server=this.app.listen(o,s.server.host,()=>{let a=this.server.address();this.assignedPort=a.port,i.info(`HTTP API listening on http://${s.server.host}:${this.assignedPort}`),this.sessionId&&this.writePortFile(this.sessionId,this.assignedPort),r(this.assignedPort)}),this.server.on("error",a=>{i.error("HTTP server error:",a),n(a)})}catch(s){n(s)}})}writePortFile(e,t){let r=re.join(ie.tmpdir(),`codevibe-claude-${e}.port`);try{B.writeFileSync(r,t.toString()),i.info(`Port file written: ${r} -> ${t}`)}catch(n){i.error(`Failed to write port file: ${r}`,n)}}removePortFile(){if(this.sessionId){let e=re.join(ie.tmpdir(),`codevibe-claude-${this.sessionId}.port`);try{B.existsSync(e)&&(B.unlinkSync(e),i.info(`Port file removed: ${e}`))}catch(t){i.warn(`Failed to remove port file: ${e}`,t)}}}async stop(e){return new Promise((t,r)=>{this.sessionId&&e?.protectedSessionIds?.has(this.sessionId)?i.info("Skipping port file removal \u2014 another daemon still serves this session",{sessionId:this.sessionId}):this.removePortFile(),this.server?this.server.close(n=>{n?(i.error("Error stopping HTTP server:",n),r(n)):(i.info("HTTP API stopped"),t())}):t()})}};var ge=require("child_process"),fe=require("@quantiya/codevibe-core");var J=class{async executePrompt(e,t){let r=(0,fe.getConfig)(),n=r.claude.defaultTimeout;return i.info("Executing prompt from mobile",{sessionId:e,promptLength:t.length,timeout:n}),new Promise(s=>{let o=["--resume",e,"--print","--output-format","stream-json",t];i.debug("Spawning Claude command",{command:r.claude.command,args:o});let a=(0,ge.spawn)(r.claude.command,o,{stdio:["pipe","pipe","pipe"],shell:!0}),c="",f="",u=!1,g=setTimeout(()=>{u=!0,i.warn("Command execution timed out",{sessionId:e,timeout:n}),a.kill("SIGTERM")},n);a.stdout?.on("data",d=>{let m=d.toString();c+=m,i.debug("Command stdout",{output:m.slice(0,200)})}),a.stderr?.on("data",d=>{let m=d.toString();f+=m,i.debug("Command stderr",{output:m.slice(0,200)})}),a.on("close",d=>{clearTimeout(g);let m={success:d===0&&!u,output:c,error:f,exitCode:d||void 0,timedOut:u};m.success?i.info("Command executed successfully",{sessionId:e,exitCode:d,outputLength:c.length}):i.error("Command execution failed",{sessionId:e,exitCode:d,timedOut:u,error:f.slice(0,500)}),s(m)}),a.on("error",d=>{clearTimeout(g),i.error("Failed to spawn command",{error:d.message}),s({success:!1,error:d.message,timedOut:!1})})})}detectInteractivePrompt(e){return[/\[Y\/n\]/i,/\[y\/N\]/i,/\(y\/n\)/i,/Continue\?/i,/Proceed\?/i].some(r=>r.test(e))}extractPromptText(e){let t=e.split(`
2
- `);for(let r=t.length-1;r>=0;r--){let n=t[r].trim();if(this.detectInteractivePrompt(n))return n}return null}};var he=require("child_process"),ye=require("util");var se=(0,ye.promisify)(he.exec),Z=class{async answerInteractivePrompt(e,t,r={}){let{pressEnter:n=!0}=r;i.info("Attempting to answer interactive prompt",{sessionId:e,response:t,pressEnter:n});try{let s=process.env.CODEVIBE_TMUX_SESSION;return i.info("Checking tmux session environment",{tmuxSession:s||"(not set)",allEnvKeys:Object.keys(process.env).filter(o=>o.includes("CODEVIBE")||o.includes("TMUX"))}),s?(i.info("Using tmux send-keys",{tmuxSession:s,pressEnter:n}),await this.sendViaTmux(s,t,n),i.info("Successfully sent response to interactive prompt",{sessionId:e,response:t,pressEnter:n}),!0):(i.error("No tmux session found - codevibe-claude wrapper is required",{sessionId:e,hint:"Start Claude Code using the codevibe-claude wrapper script"}),!1)}catch(s){return i.error("Failed to answer interactive prompt",{sessionId:e,error:s instanceof Error?s.message:String(s)}),!1}}async sendViaTmux(e,t,r){let n=t.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");i.info("Sending via tmux",{sessionName:e,inputLength:t.length,pressEnter:r});try{let s=`tmux send-keys -t "${e}" -l "${n}"`,o=await se(s);if(i.info("tmux send-keys (text) completed",{stdout:o.stdout||"(empty)",stderr:o.stderr||"(empty)"}),r){await this.delay(500);let a=`tmux send-keys -t "${e}" Enter`,c=await se(a);i.info("tmux send-keys (Enter) completed",{stdout:c.stdout||"(empty)",stderr:c.stderr||"(empty)"})}else i.info("tmux send-keys: skipping Enter (caller requested digit-only)")}catch(s){throw i.error("tmux send-keys failed",{sessionName:e,error:s}),s}}async sendKey(e,t){let r=process.env.CODEVIBE_TMUX_SESSION;if(!r)return i.error("No tmux session found for sendKey",{sessionId:e,keyName:t}),!1;try{let n=`tmux send-keys -t "${r}" ${t}`,s=await se(n);return i.info("tmux send-keys (single key) completed",{sessionId:e,keyName:t,stdout:s.stdout||"(empty)",stderr:s.stderr||"(empty)"}),!0}catch(n){return i.error("tmux send-keys (single key) failed",{sessionId:e,keyName:t,error:n instanceof Error?n.message:String(n)}),!1}}delay(e){return new Promise(t=>setTimeout(t,e))}isPromptResponse(e){let t=e.trim().toLowerCase();return!!(t==="y"||t==="n"||t==="yes"||t==="no"||/^[0-9]+$/.test(t)||/^[a-z]$/.test(t)||["exit","quit","q","continue","skip","abort","retry","cancel"].includes(t))}};var _e=(0,Ee.promisify)(Se.exec),Re="/exit",we="CODEVIBE_TMUX_SESSION";async function Ue(w,e){let t=async(r,n)=>{try{await _e(r)}catch(s){i.warn("tmux send-keys failed during self-terminate",{sessionName:w,label:n,error:String(s)})}};await t(`tmux send-keys -t "${w}" C-c`,"ctrl-c"),await new Promise(r=>setTimeout(r,200)),await t(`tmux send-keys -t "${w}" -l "${e}"`,"quit-text"),await new Promise(r=>setTimeout(r,500)),await t(`tmux send-keys -t "${w}" Enter`,"enter")}var Fe={question:"Ready to submit your answers?",options:[{label:"Submit answers",description:"Send your selections to the assistant"},{label:"Cancel",description:"Discard your answers"}],multiSelect:!1,_isSubmit:!0};function Me(w){let e=w.length,t=e>=2,r=w.slice(1);return t&&r.push({...Fe}),{questionCount:e,hasReviewScreen:t,remainingQueue:r}}var te=class w{constructor(e){this.activeSessions=new Map;this.assignedPort=0;this.sessionKey=null;this.claudeToBackendSessionId=new Map;this.pendingMobilePrompts=new Map;this.nextPromptGen=1;this.httpApi=new z,this.commandExecutor=new J,this.promptResponder=new Z,this.initialSessionId=e}static{this.MOBILE_PROMPT_EXPIRY_MS=3e3}getPort(){return this.assignedPort}generateBackendSessionId(e){return`claude-${e}`}trackMobilePrompt(e,t){this.pendingMobilePrompts.has(e)||this.pendingMobilePrompts.set(e,[]),this.pendingMobilePrompts.get(e).push({prompt:t.trim(),timestamp:Date.now()}),i.debug("Tracking mobile prompt for deduplication",{sessionId:e,promptLength:t.length})}isRecentMobilePrompt(e,t){let r=this.pendingMobilePrompts.get(e);if(!r)return!1;let n=Date.now(),s=t.trim(),o=[],a=!1;for(let c of r)if(!(n-c.timestamp>w.MOBILE_PROMPT_EXPIRY_MS)){if(!a&&c.prompt===s){a=!0,i.debug("Found matching mobile prompt, filtering duplicate",{sessionId:e});continue}o.push(c)}return o.length>0?this.pendingMobilePrompts.set(e,o):this.pendingMobilePrompts.delete(e),a}writePortFile(e){let t=V.join(ee.tmpdir(),`codevibe-claude-${e}.port`);try{D.writeFileSync(t,this.assignedPort.toString()),i.info(`Port file written: ${t} -> ${this.assignedPort}`)}catch(r){i.error(`Failed to write port file: ${t}`,r)}}removePortFile(e){let t=V.join(ee.tmpdir(),`codevibe-claude-${e}.port`);try{D.existsSync(t)&&(D.unlinkSync(t),i.info(`Port file removed: ${t}`))}catch(r){i.warn(`Failed to remove port file: ${t}`,r)}}hasOtherLiveDaemonForSession(e){try{let t=(0,ve.execSync)("ps -eww -o pid= -o args=",{encoding:"utf8",timeout:2e3}),r=process.pid;for(let n of t.split(`
3
- `)){let s=n.trim();if(!s)continue;let o=s.indexOf(" ");if(o<0)continue;let a=parseInt(s.substring(0,o),10);if(isNaN(a)||a===r)continue;let c=s.substring(o+1);if(/node.*codevibe-claude.*server\.js/.test(c)&&c.includes(e))return!0}return!1}catch(t){return i.warn('hasOtherLiveDaemonForSession: ps query failed; falling back to "no other daemon"',{error:String(t)}),!1}}async start(){try{if(i.info("Starting CodeVibe MCP Server...",{environment:(0,p.getEnvironment)()}),this.appSyncClient=new p.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()){i.info("Authenticated with stored OAuth tokens",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,p.registerDeviceEncryptionKey)(this.appSyncClient,i),(0,p.startDeviceKeyWatcher)(this.appSyncClient,i);try{let t=await this.appSyncClient.sweepOrphanSessions({agentType:"CLAUDE"});t>0&&i.info("Orphan sweep: marked stale Claude sessions INACTIVE",{swept:t})}catch(t){i.warn("Orphan sweep failed, continuing startup",{error:t instanceof Error?t.message:String(t)})}}else i.error('Authentication failed. Run "codevibe-claude login" first.'),console.error('Not authenticated. Run "codevibe-claude login" to sign in.'),process.exit(1);this.httpApi.onEvent(this.handleEventFromHook.bind(this)),this.assignedPort=await this.httpApi.start(this.initialSessionId),i.info("MCP Server started successfully",{port:this.assignedPort,host:(0,p.getConfig)().server.host,dynamicPort:(0,p.getConfig)().server.dynamicPort,sessionId:this.initialSessionId,authenticated:this.appSyncClient.isAuthenticated(),userId:this.appSyncClient.getCurrentUserId()})}catch(e){throw i.error("Failed to start MCP Server:",e),e}}async stop(){i.info("Stopping MCP Server...");let e=Array.from(this.activeSessions.keys()),t=new Set;i.info(`Marking ${e.length} active session(s) as INACTIVE...`);for(let r of e){let n=this.activeSessions.get(r);n?.mobileEndWatcher&&(n.mobileEndWatcher.stop(),n.mobileEndWatcher=void 0)}for(let r of e)try{let n=this.activeSessions.get(r);if(n&&this.hasOtherLiveDaemonForSession(n.claudeSessionId)){i.info("Another daemon serves this session \u2014 skipping mark INACTIVE AND port file removal during shutdown",{sessionId:r,claudeSessionId:n.claudeSessionId,myPid:process.pid}),t.add(n.claudeSessionId);continue}await this.appSyncClient.updateSession({sessionId:r,status:p.SessionStatus.INACTIVE}),i.info("Session marked as INACTIVE during shutdown",{sessionId:r}),n&&this.removePortFile(n.claudeSessionId)}catch(n){i.warn("Failed to mark session as INACTIVE during shutdown",{sessionId:r,error:n})}this.appSyncClient.cleanupSubscriptions(),this.activeSessions.clear(),await this.httpApi.stop({protectedSessionIds:t}),i.info("MCP Server stopped")}async handleEventFromHook(e){let{session_id:t,hook_event_name:r,type:n,content:s}=e;i.info("Processing hook event",{sessionId:t,hookEvent:r,type:n});try{r==="SessionStart"?await this.handleSessionStart(e):r==="SessionEnd"&&await this.handleSessionEnd(e);let o=this.claudeToBackendSessionId.get(t)||this.generateBackendSessionId(t);if(r==="UserPromptSubmit"){let g=this.activeSessions.get(o);if(g?.completedAskUserQuestionFingerprints?.size){let d=g.completedAskUserQuestionFingerprints.size;g.completedAskUserQuestionFingerprints.clear(),i.info("Turn boundary \u2014 cleared closed-AskUserQuestion fingerprints",{sessionId:o,clearedCount:d})}}if(n===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP&&r==="UserPromptSubmit"&&s&&this.isRecentMobilePrompt(o,s)){i.info("Skipping duplicate USER_PROMPT from mobile-originated prompt",{sessionId:o,contentLength:s.length});return}if(n===p.EventType.INTERACTIVE_PROMPT){(typeof e.prompt_id!="string"||e.prompt_id.length===0)&&(e.prompt_id=`synth-${(0,W.randomUUID)()}`,i.info("Synthesized prompt_id for INTERACTIVE_PROMPT (hook omitted it)",{sessionId:o,synthesizedPromptId:e.prompt_id}));let g=this.activeSessions.get(o),d;if(g&&e.metadata?.tool_name==="AskUserQuestion"){if(d=this.computeAskUserQuestionFingerprint(e.metadata.tool_input?.questions),d){let Q=g.activeAskUserQuestionFingerprint===d,v=g.completedAskUserQuestionFingerprints?.has(d)??!1;if(Q||v){i.info("Dropping duplicate INTERACTIVE_PROMPT \u2014 AskUserQuestion already tracked",{sessionId:o,fingerprint:d.slice(0,16),status:Q?"in-flight":"completed",hookEvent:e.hook_event_name,promptId:e.prompt_id});return}}let m=e.metadata.tool_input?.questions,x=Array.isArray(m)&&m.some(Q=>Q?.multiSelect===!0),R=Array.isArray(m)&&(m[0]?.options?.length??0)===0;if(d&&(x||R)){g.completedAskUserQuestionFingerprints||(g.completedAskUserQuestionFingerprints=new Set),g.completedAskUserQuestionFingerprints.add(d);let Q=x?"\u26A0\uFE0F This AskUserQuestion uses multi-select, which can't be answered from mobile. Please answer it on your desktop terminal.":"\u26A0\uFE0F This AskUserQuestion has no explicit options \u2014 please answer on your desktop terminal.";i.info("AUQ degraded at intercept \u2014 emitting notification, skipping walker setup",{sessionId:o,fingerprint:d.slice(0,16),hasMultiSelect:x,isZeroExplicitOption:R,questionCount:Array.isArray(m)?m.length:0}),setImmediate(()=>this.emitDegradedAUQNotification(o,Q));return}}if(g){this.clearPromptState(g),g.waitingForPromptResponse=!0,g.pendingPromptId=e.prompt_id;let m=this.nextPromptGen++;g.promptGenerationToken={promptId:e.prompt_id||"",gen:m},d&&(g.activeAskUserQuestionFingerprint=d),i.info("Interactive prompt detected - will parse options from tmux",{sessionId:o,promptId:e.prompt_id,tokenGen:m,askUserQuestionFingerprint:d?.slice(0,16)})}this.sendInteractivePromptAsync(o,e,s).catch(m=>{i.error("Failed to send interactive prompt with dynamic options",{error:m})});return}let a=s,c=e.metadata,f=!1;i.info("Hook event encryption state",{type:n,sessionId:o,hasSessionKey:!!this.sessionKey,sessionKeyLength:this.sessionKey?.length||0}),this.sessionKey?(a=p.cryptoService.encryptContent(s,this.sessionKey),c&&(c={encrypted:p.cryptoService.encryptMetadata(c,this.sessionKey)}),f=!0,i.info("Event encrypted for hook",{type:n,sessionId:o,isEncrypted:!0})):i.warn("No session key - event will NOT be encrypted",{type:n,sessionId:o});let u=await this.appSyncClient.createEvent({sessionId:o,type:n,source:e.source,content:a,metadata:c,promptId:e.prompt_id,timestamp:(0,p.prepareEventTimestamp)({orderingKey:o}),isEncrypted:f?!0:void 0});if(n===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP){let g=this.activeSessions.get(o);g?.waitingForPromptResponse&&(this.promoteFingerprintAndClearPromptState(g),i.info("Clearing prompt wait state - new desktop prompt received",{sessionId:o}))}i.debug("Event sent to AppSync successfully")}catch(o){throw i.error("Failed to process hook event:",o),o}}async handleSessionStart(e){let t=e.session_id,r=this.generateBackendSessionId(t),n=e.metadata?.cwd||process.cwd();this.claudeToBackendSessionId.set(t,r),i.info("Session started",{claudeSessionId:t,sessionId:r,cwd:n});let s=Array.from(this.activeSessions.keys()).filter(f=>f!==r);if(s.length>0){i.info(`Marking ${s.length} previous session(s) as INACTIVE`);for(let f of s){let u=this.activeSessions.get(f);u?.mobileEndWatcher&&(u.mobileEndWatcher.stop(),u.mobileEndWatcher=void 0);try{await this.appSyncClient.updateSession({sessionId:f,status:p.SessionStatus.INACTIVE}),i.info("Previous session marked INACTIVE",{prevId:f,newSessionId:r})}catch(g){i.warn("Failed to mark previous session as INACTIVE",{prevId:f,error:g})}u&&this.removePortFile(u.claudeSessionId),this.activeSessions.delete(f)}}this.writePortFile(t);let o=this.appSyncClient.getCurrentUserId(),a={sessionId:r,claudeSessionId:t,userId:o,projectPath:n,cwd:n,createdAt:new Date,subscriptionActive:!1,waitingForPromptResponse:!1,metadata:e.metadata||{}};this.activeSessions.set(r,a);try{let f=await(0,p.resumeOrCreateSession)({sessionId:r,userId:a.userId,agentType:p.AgentType.CLAUDE,projectPath:n,metadata:e.metadata||{}},this.appSyncClient,i);if(this.sessionKey=f.sessionKey,f.resumed&&!f.sessionKey){let u=await p.keychainManager.getDeviceId();i.error("Device key not found in session encryptedKeys",{sessionId:r,pluginDeviceId:u}),console.error(`
1
+ "use strict";var ke=Object.create;var X=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Ie=Object.getOwnPropertyNames;var Ce=Object.getPrototypeOf,Ae=Object.prototype.hasOwnProperty;var Te=(w,e)=>{for(var t in e)X(w,t,{get:e[t],enumerable:!0})},ce=(w,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of Ie(e))!Ae.call(w,n)&&n!==t&&X(w,n,{get:()=>e[n],enumerable:!(r=be(e,n))||r.enumerable});return w};var O=(w,e,t)=>(t=w!=null?ke(Ce(w)):{},ce(e||!w||!w.__esModule?X(t,"default",{value:w,enumerable:!0}):t,w)),xe=w=>ce(X({},"__esModule",{value:!0}),w);var Oe={};Te(Oe,{McpServer:()=>te,parseInteractivePromptInput:()=>Pe});module.exports=xe(Oe);var D=O(require("fs")),V=O(require("path")),ee=O(require("os")),ve=require("child_process"),Ee=require("util"),Se=require("child_process"),W=require("crypto");var de=O(require("os")),le=O(require("path")),ue=require("@quantiya/codevibe-core"),i=(0,ue.createLogger)({name:"codevibe-claude",logFile:le.default.join(de.default.tmpdir(),"codevibe-claude-mcp.log"),level:"info"});var p=require("@quantiya/codevibe-core");var ne=O(require("express")),B=O(require("fs")),re=O(require("path")),ie=O(require("os")),me=require("@quantiya/codevibe-core");var S=require("@quantiya/codevibe-core");var z=class{constructor(){this.assignedPort=0;this.app=(0,ne.default)(),this.setupMiddleware(),this.setupRoutes()}setSessionId(e){this.sessionId=e}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(ne.default.json({limit:"1mb"})),this.app.use((e,t,r)=>{i.debug(`${e.method} ${e.path}`,{body:e.body,query:e.query}),r()}),this.app.use((e,t,r,n)=>{i.error("Express error:",e);let s={success:!1,error:e.message||"Internal server error"};r.status(500).json(s)})}setupRoutes(){this.app.get("/health",this.handleHealth.bind(this)),this.app.post("/event",this.handleEvent.bind(this)),process.env.NODE_ENV!=="production"&&this.app.post("/test/execute",this.handleTestExecute.bind(this))}handleHealth(e,t){let r={success:!0,data:{status:"healthy",uptime:process.uptime(),version:"0.1.0",timestamp:new Date().toISOString()}};t.json(r)}async handleEvent(e,t){try{let r=e.body;if(!r.session_id){let o={success:!1,error:"Missing required field: session_id"};t.status(400).json(o);return}if(!r.hook_event_name){let o={success:!1,error:"Missing required field: hook_event_name"};t.status(400).json(o);return}let n=this.transformHookToEvent(r);i.info("Received event from hook",{sessionId:r.session_id,hookEvent:r.hook_event_name,type:n.type}),this.eventHandler?await this.eventHandler(n):i.warn("No event handler registered");let s={success:!0,message:"Event processed successfully"};t.json(s)}catch(r){i.error("Error handling event:",r);let n={success:!1,error:r instanceof Error?r.message:"Unknown error"};t.status(500).json(n)}}async handleTestExecute(e,t){try{let{sessionId:r,prompt:n}=e.body;if(!r||!n){let o={success:!1,error:"Missing required fields: sessionId, prompt"};t.status(400).json(o);return}i.info("Test execute request",{sessionId:r,prompt:n});let s={success:!0,message:"Test execution endpoint - not implemented yet",data:{sessionId:r,prompt:n}};t.json(s)}catch(r){i.error("Error in test execute:",r);let n={success:!1,error:r instanceof Error?r.message:"Unknown error"};t.status(500).json(n)}}transformHookToEvent(e){let t,r,n={cwd:e.cwd,hook_event_name:e.hook_event_name,...e.metadata||{}};if(e.type&&e.content!==void 0)t=e.type,r=e.content;else switch(e.hook_event_name){case"SessionStart":t=S.EventType.NOTIFICATION,r="Session started",n.source=e.source;break;case"SessionEnd":t=S.EventType.NOTIFICATION,r=`Session ended: ${e.reason||"unknown"}`,n.reason=e.reason;break;case"UserPromptSubmit":t=S.EventType.USER_PROMPT,r=e.prompt||"";break;case"PostToolUse":t=S.EventType.TOOL_USE,r=JSON.stringify({tool_name:e.tool_name,tool_input:e.tool_input,tool_response:e.tool_response}),n.tool_name=e.tool_name;break;case"Notification":t=S.EventType.NOTIFICATION,r=e.message||"",n.notification_type=e.notification_type;break;default:t=S.EventType.NOTIFICATION,r=`Hook event: ${e.hook_event_name}`}return{session_id:e.session_id,hook_event_name:e.hook_event_name,type:t,source:S.EventSource.DESKTOP,content:r,metadata:n}}onEvent(e){this.eventHandler=e}async start(e){let t=e||this.sessionId;return t&&(this.sessionId=t),new Promise((r,n)=>{try{let s=(0,me.getConfig)(),o=s.server.dynamicPort?0:s.server.port;this.server=this.app.listen(o,s.server.host,()=>{let a=this.server.address();this.assignedPort=a.port,i.info(`HTTP API listening on http://${s.server.host}:${this.assignedPort}`),this.sessionId&&this.writePortFile(this.sessionId,this.assignedPort),r(this.assignedPort)}),this.server.on("error",a=>{i.error("HTTP server error:",a),n(a)})}catch(s){n(s)}})}writePortFile(e,t){let r=re.join(ie.tmpdir(),`codevibe-claude-${e}.port`);try{B.writeFileSync(r,t.toString()),i.info(`Port file written: ${r} -> ${t}`)}catch(n){i.error(`Failed to write port file: ${r}`,n)}}removePortFile(){if(this.sessionId){let e=re.join(ie.tmpdir(),`codevibe-claude-${this.sessionId}.port`);try{B.existsSync(e)&&(B.unlinkSync(e),i.info(`Port file removed: ${e}`))}catch(t){i.warn(`Failed to remove port file: ${e}`,t)}}}async stop(e){return new Promise((t,r)=>{this.sessionId&&e?.protectedSessionIds?.has(this.sessionId)?i.info("Skipping port file removal \u2014 another daemon still serves this session",{sessionId:this.sessionId}):this.removePortFile(),this.server?this.server.close(n=>{n?(i.error("Error stopping HTTP server:",n),r(n)):(i.info("HTTP API stopped"),t())}):t()})}};var ge=require("child_process"),he=require("@quantiya/codevibe-core");var J=class{async executePrompt(e,t){let r=(0,he.getConfig)(),n=r.claude.defaultTimeout;return i.info("Executing prompt from mobile",{sessionId:e,promptLength:t.length,timeout:n}),new Promise(s=>{let o=["--resume",e,"--print","--output-format","stream-json",t];i.debug("Spawning Claude command",{command:r.claude.command,args:o});let a=(0,ge.spawn)(r.claude.command,o,{stdio:["pipe","pipe","pipe"],shell:!0}),c="",h="",u=!1,g=setTimeout(()=>{u=!0,i.warn("Command execution timed out",{sessionId:e,timeout:n}),a.kill("SIGTERM")},n);a.stdout?.on("data",d=>{let m=d.toString();c+=m,i.debug("Command stdout",{output:m.slice(0,200)})}),a.stderr?.on("data",d=>{let m=d.toString();h+=m,i.debug("Command stderr",{output:m.slice(0,200)})}),a.on("close",d=>{clearTimeout(g);let m={success:d===0&&!u,output:c,error:h,exitCode:d||void 0,timedOut:u};m.success?i.info("Command executed successfully",{sessionId:e,exitCode:d,outputLength:c.length}):i.error("Command execution failed",{sessionId:e,exitCode:d,timedOut:u,error:h.slice(0,500)}),s(m)}),a.on("error",d=>{clearTimeout(g),i.error("Failed to spawn command",{error:d.message}),s({success:!1,error:d.message,timedOut:!1})})})}detectInteractivePrompt(e){return[/\[Y\/n\]/i,/\[y\/N\]/i,/\(y\/n\)/i,/Continue\?/i,/Proceed\?/i].some(r=>r.test(e))}extractPromptText(e){let t=e.split(`
2
+ `);for(let r=t.length-1;r>=0;r--){let n=t[r].trim();if(this.detectInteractivePrompt(n))return n}return null}};var fe=require("child_process"),ye=require("util");var se=(0,ye.promisify)(fe.exec),Z=class{async answerInteractivePrompt(e,t,r={}){let{pressEnter:n=!0}=r;i.info("Attempting to answer interactive prompt",{sessionId:e,response:t,pressEnter:n});try{let s=process.env.CODEVIBE_TMUX_SESSION;return i.info("Checking tmux session environment",{tmuxSession:s||"(not set)",allEnvKeys:Object.keys(process.env).filter(o=>o.includes("CODEVIBE")||o.includes("TMUX"))}),s?(i.info("Using tmux send-keys",{tmuxSession:s,pressEnter:n}),await this.sendViaTmux(s,t,n),i.info("Successfully sent response to interactive prompt",{sessionId:e,response:t,pressEnter:n}),!0):(i.error("No tmux session found - codevibe-claude wrapper is required",{sessionId:e,hint:"Start Claude Code using the codevibe-claude wrapper script"}),!1)}catch(s){return i.error("Failed to answer interactive prompt",{sessionId:e,error:s instanceof Error?s.message:String(s)}),!1}}async sendViaTmux(e,t,r){let n=t.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");i.info("Sending via tmux",{sessionName:e,inputLength:t.length,pressEnter:r});try{let s=`tmux send-keys -t "${e}" -l "${n}"`,o=await se(s);if(i.info("tmux send-keys (text) completed",{stdout:o.stdout||"(empty)",stderr:o.stderr||"(empty)"}),r){await this.delay(500);let a=`tmux send-keys -t "${e}" Enter`,c=await se(a);i.info("tmux send-keys (Enter) completed",{stdout:c.stdout||"(empty)",stderr:c.stderr||"(empty)"})}else i.info("tmux send-keys: skipping Enter (caller requested digit-only)")}catch(s){throw i.error("tmux send-keys failed",{sessionName:e,error:s}),s}}async sendKey(e,t){let r=process.env.CODEVIBE_TMUX_SESSION;if(!r)return i.error("No tmux session found for sendKey",{sessionId:e,keyName:t}),!1;try{let n=`tmux send-keys -t "${r}" ${t}`,s=await se(n);return i.info("tmux send-keys (single key) completed",{sessionId:e,keyName:t,stdout:s.stdout||"(empty)",stderr:s.stderr||"(empty)"}),!0}catch(n){return i.error("tmux send-keys (single key) failed",{sessionId:e,keyName:t,error:n instanceof Error?n.message:String(n)}),!1}}delay(e){return new Promise(t=>setTimeout(t,e))}isPromptResponse(e){let t=e.trim().toLowerCase();return!!(t==="y"||t==="n"||t==="yes"||t==="no"||/^[0-9]+$/.test(t)||/^[a-z]$/.test(t)||["exit","quit","q","continue","skip","abort","retry","cancel"].includes(t))}};var _e=(0,Ee.promisify)(Se.exec),Re="/exit",we="CODEVIBE_TMUX_SESSION";async function Ue(w,e){let t=async(r,n)=>{try{await _e(r)}catch(s){i.warn("tmux send-keys failed during self-terminate",{sessionName:w,label:n,error:String(s)})}};await t(`tmux send-keys -t "${w}" C-c`,"ctrl-c"),await new Promise(r=>setTimeout(r,200)),await t(`tmux send-keys -t "${w}" -l "${e}"`,"quit-text"),await new Promise(r=>setTimeout(r,500)),await t(`tmux send-keys -t "${w}" Enter`,"enter")}var Fe={question:"Ready to submit your answers?",options:[{label:"Submit answers",description:"Send your selections to the assistant"},{label:"Cancel",description:"Discard your answers"}],multiSelect:!1,_isSubmit:!0};function Me(w){let e=w.length,t=e>=2,r=w.slice(1);return t&&r.push({...Fe}),{questionCount:e,hasReviewScreen:t,remainingQueue:r}}var te=class w{constructor(e){this.activeSessions=new Map;this.assignedPort=0;this.sessionKey=null;this.claudeToBackendSessionId=new Map;this.pendingMobilePrompts=new Map;this.nextPromptGen=1;this.httpApi=new z,this.commandExecutor=new J,this.promptResponder=new Z,this.initialSessionId=e}static{this.MOBILE_PROMPT_EXPIRY_MS=3e3}getPort(){return this.assignedPort}generateBackendSessionId(e){return`claude-${e}`}trackMobilePrompt(e,t){this.pendingMobilePrompts.has(e)||this.pendingMobilePrompts.set(e,[]),this.pendingMobilePrompts.get(e).push({prompt:t.trim(),timestamp:Date.now()}),i.debug("Tracking mobile prompt for deduplication",{sessionId:e,promptLength:t.length})}isRecentMobilePrompt(e,t){let r=this.pendingMobilePrompts.get(e);if(!r)return!1;let n=Date.now(),s=t.trim(),o=[],a=!1;for(let c of r)if(!(n-c.timestamp>w.MOBILE_PROMPT_EXPIRY_MS)){if(!a&&c.prompt===s){a=!0,i.debug("Found matching mobile prompt, filtering duplicate",{sessionId:e});continue}o.push(c)}return o.length>0?this.pendingMobilePrompts.set(e,o):this.pendingMobilePrompts.delete(e),a}writePortFile(e){let t=V.join(ee.tmpdir(),`codevibe-claude-${e}.port`);try{D.writeFileSync(t,this.assignedPort.toString()),i.info(`Port file written: ${t} -> ${this.assignedPort}`)}catch(r){i.error(`Failed to write port file: ${t}`,r)}}removePortFile(e){let t=V.join(ee.tmpdir(),`codevibe-claude-${e}.port`);try{D.existsSync(t)&&(D.unlinkSync(t),i.info(`Port file removed: ${t}`))}catch(r){i.warn(`Failed to remove port file: ${t}`,r)}}hasOtherLiveDaemonForSession(e){try{let t=(0,ve.execSync)("ps -eww -o pid= -o args=",{encoding:"utf8",timeout:2e3}),r=process.pid;for(let n of t.split(`
3
+ `)){let s=n.trim();if(!s)continue;let o=s.indexOf(" ");if(o<0)continue;let a=parseInt(s.substring(0,o),10);if(isNaN(a)||a===r)continue;let c=s.substring(o+1);if(/node.*codevibe-claude.*server\.js/.test(c)&&c.includes(e))return!0}return!1}catch(t){return i.warn('hasOtherLiveDaemonForSession: ps query failed; falling back to "no other daemon"',{error:String(t)}),!1}}async start(){try{if(i.info("Starting CodeVibe MCP Server...",{environment:(0,p.getEnvironment)()}),this.appSyncClient=new p.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()){i.info("Authenticated with stored OAuth tokens",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,p.registerDeviceEncryptionKey)(this.appSyncClient,i),(0,p.startDeviceKeyWatcher)(this.appSyncClient,i);try{let t=await this.appSyncClient.sweepOrphanSessions({agentType:"CLAUDE"});t>0&&i.info("Orphan sweep: marked stale Claude sessions INACTIVE",{swept:t})}catch(t){i.warn("Orphan sweep failed, continuing startup",{error:t instanceof Error?t.message:String(t)})}}else i.error('Authentication failed. Run "codevibe-claude login" first.'),console.error('Not authenticated. Run "codevibe-claude login" to sign in.'),process.exit(1);this.httpApi.onEvent(this.handleEventFromHook.bind(this)),this.assignedPort=await this.httpApi.start(this.initialSessionId),i.info("MCP Server started successfully",{port:this.assignedPort,host:(0,p.getConfig)().server.host,dynamicPort:(0,p.getConfig)().server.dynamicPort,sessionId:this.initialSessionId,authenticated:this.appSyncClient.isAuthenticated(),userId:this.appSyncClient.getCurrentUserId()})}catch(e){throw i.error("Failed to start MCP Server:",e),e}}async stop(){i.info("Stopping MCP Server...");let e=Array.from(this.activeSessions.keys()),t=new Set;i.info(`Marking ${e.length} active session(s) as INACTIVE...`);for(let r of e){let n=this.activeSessions.get(r);n?.mobileEndWatcher&&(n.mobileEndWatcher.stop(),n.mobileEndWatcher=void 0)}for(let r of e)try{let n=this.activeSessions.get(r);if(n&&this.hasOtherLiveDaemonForSession(n.claudeSessionId)){i.info("Another daemon serves this session \u2014 skipping mark INACTIVE AND port file removal during shutdown",{sessionId:r,claudeSessionId:n.claudeSessionId,myPid:process.pid}),t.add(n.claudeSessionId);continue}await this.appSyncClient.updateSession({sessionId:r,status:p.SessionStatus.INACTIVE}),i.info("Session marked as INACTIVE during shutdown",{sessionId:r}),n&&this.removePortFile(n.claudeSessionId)}catch(n){i.warn("Failed to mark session as INACTIVE during shutdown",{sessionId:r,error:n})}this.appSyncClient.cleanupSubscriptions(),this.activeSessions.clear(),await this.httpApi.stop({protectedSessionIds:t}),i.info("MCP Server stopped")}async handleEventFromHook(e){let{session_id:t,hook_event_name:r,type:n,content:s}=e;i.info("Processing hook event",{sessionId:t,hookEvent:r,type:n});try{r==="SessionStart"?await this.handleSessionStart(e):r==="SessionEnd"&&await this.handleSessionEnd(e);let o=this.claudeToBackendSessionId.get(t)||this.generateBackendSessionId(t);if(r==="UserPromptSubmit"){let g=this.activeSessions.get(o);if(g?.completedAskUserQuestionFingerprints?.size){let d=g.completedAskUserQuestionFingerprints.size;g.completedAskUserQuestionFingerprints.clear(),i.info("Turn boundary \u2014 cleared closed-AskUserQuestion fingerprints",{sessionId:o,clearedCount:d})}}if(n===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP&&r==="UserPromptSubmit"&&s&&this.isRecentMobilePrompt(o,s)){i.info("Skipping duplicate USER_PROMPT from mobile-originated prompt",{sessionId:o,contentLength:s.length});return}if(n===p.EventType.INTERACTIVE_PROMPT){(typeof e.prompt_id!="string"||e.prompt_id.length===0)&&(e.prompt_id=`synth-${(0,W.randomUUID)()}`,i.info("Synthesized prompt_id for INTERACTIVE_PROMPT (hook omitted it)",{sessionId:o,synthesizedPromptId:e.prompt_id}));let g=this.activeSessions.get(o),d;if(g&&e.metadata?.tool_name==="AskUserQuestion"){if(d=this.computeAskUserQuestionFingerprint(e.metadata.tool_input?.questions),d){let Q=g.activeAskUserQuestionFingerprint===d,v=g.completedAskUserQuestionFingerprints?.has(d)??!1;if(Q||v){i.info("Dropping duplicate INTERACTIVE_PROMPT \u2014 AskUserQuestion already tracked",{sessionId:o,fingerprint:d.slice(0,16),status:Q?"in-flight":"completed",hookEvent:e.hook_event_name,promptId:e.prompt_id});return}}let m=e.metadata.tool_input?.questions,x=Array.isArray(m)&&m.some(Q=>Q?.multiSelect===!0),R=Array.isArray(m)&&(m[0]?.options?.length??0)===0;if(d&&(x||R)){g.completedAskUserQuestionFingerprints||(g.completedAskUserQuestionFingerprints=new Set),g.completedAskUserQuestionFingerprints.add(d);let Q=x?"\u26A0\uFE0F This AskUserQuestion uses multi-select, which can't be answered from mobile. Please answer it on your desktop terminal.":"\u26A0\uFE0F This AskUserQuestion has no explicit options \u2014 please answer on your desktop terminal.";i.info("AUQ degraded at intercept \u2014 emitting notification, skipping walker setup",{sessionId:o,fingerprint:d.slice(0,16),hasMultiSelect:x,isZeroExplicitOption:R,questionCount:Array.isArray(m)?m.length:0}),setImmediate(()=>this.emitDegradedAUQNotification(o,Q));return}}if(g){this.clearPromptState(g),g.waitingForPromptResponse=!0,g.pendingPromptId=e.prompt_id;let m=this.nextPromptGen++;g.promptGenerationToken={promptId:e.prompt_id||"",gen:m},d&&(g.activeAskUserQuestionFingerprint=d),i.info("Interactive prompt detected - will parse options from tmux",{sessionId:o,promptId:e.prompt_id,tokenGen:m,askUserQuestionFingerprint:d?.slice(0,16)})}this.sendInteractivePromptAsync(o,e,s).catch(m=>{i.error("Failed to send interactive prompt with dynamic options",{error:m})});return}let a=s,c=e.metadata,h=!1;i.info("Hook event encryption state",{type:n,sessionId:o,hasSessionKey:!!this.sessionKey,sessionKeyLength:this.sessionKey?.length||0}),this.sessionKey?(a=p.cryptoService.encryptContent(s,this.sessionKey),c&&(c={encrypted:p.cryptoService.encryptMetadata(c,this.sessionKey)}),h=!0,i.info("Event encrypted for hook",{type:n,sessionId:o,isEncrypted:!0})):i.warn("No session key - event will NOT be encrypted",{type:n,sessionId:o});let u=await this.appSyncClient.createEvent({sessionId:o,type:n,source:e.source,content:a,metadata:c,promptId:e.prompt_id,timestamp:(0,p.prepareEventTimestamp)({orderingKey:o}),isEncrypted:h?!0:void 0});if(n===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP){let g=this.activeSessions.get(o);g?.waitingForPromptResponse&&(this.promoteFingerprintAndClearPromptState(g),i.info("Clearing prompt wait state - new desktop prompt received",{sessionId:o}))}i.debug("Event sent to AppSync successfully")}catch(o){throw i.error("Failed to process hook event:",o),o}}async handleSessionStart(e){let t=e.session_id,r=this.generateBackendSessionId(t),n=e.metadata?.cwd||process.cwd();this.claudeToBackendSessionId.set(t,r),i.info("Session started",{claudeSessionId:t,sessionId:r,cwd:n});let s=Array.from(this.activeSessions.keys()).filter(h=>h!==r);if(s.length>0){i.info(`Marking ${s.length} previous session(s) as INACTIVE`);for(let h of s){let u=this.activeSessions.get(h);u?.mobileEndWatcher&&(u.mobileEndWatcher.stop(),u.mobileEndWatcher=void 0);try{await this.appSyncClient.updateSession({sessionId:h,status:p.SessionStatus.INACTIVE}),i.info("Previous session marked INACTIVE",{prevId:h,newSessionId:r})}catch(g){i.warn("Failed to mark previous session as INACTIVE",{prevId:h,error:g})}u&&this.removePortFile(u.claudeSessionId),this.activeSessions.delete(h)}}this.writePortFile(t);let o=this.appSyncClient.getCurrentUserId(),a={sessionId:r,claudeSessionId:t,userId:o,projectPath:n,cwd:n,createdAt:new Date,subscriptionActive:!1,waitingForPromptResponse:!1,metadata:e.metadata||{}};this.activeSessions.set(r,a);try{let h=await(0,p.resumeOrCreateSession)({sessionId:r,userId:a.userId,agentType:p.AgentType.CLAUDE,projectPath:n,metadata:e.metadata||{}},this.appSyncClient,i);if(this.sessionKey=h.sessionKey,h.resumed&&!h.sessionKey){let u=await p.keychainManager.getDeviceId();i.error("Device key not found in session encryptedKeys",{sessionId:r,pluginDeviceId:u}),console.error(`
4
4
  \u26A0\uFE0F E2E ENCRYPTION WARNING: Cannot decrypt this session!`),console.error(` Your device ID (${u.substring(0,8)}...) is not in session's encryption keys.`),console.error(" This happens if your device key was regenerated after the session was created."),console.error(` SOLUTION: Start a new Claude Code session instead of resuming this one.
5
- `)}}catch(f){if(this.isSessionLimitExceeded(f)){this.displaySubscriptionLimitError(f,"session"),this.activeSessions.delete(r),this.removePortFile(t);return}i.error("Failed to create/resume session:",f)}this.subscribeToMobileEvents(r),this.appSyncClient.startHeartbeat(r);let c=this.activeSessions.get(r);c&&(c.mobileEndWatcher=this.appSyncClient.watchForMobileEnd(r,async()=>{i.info("Mobile ended session \u2014 sending desktop quit",{sessionId:r});let f=process.env[we];if(!f){i.warn("No tmux session set; skipping desktop self-terminate",{sessionId:r,expectedEnv:we});return}await Ue(f,Re)}))}async handleSessionEnd(e){let t=e.session_id,r=this.claudeToBackendSessionId.get(t)||this.generateBackendSessionId(t);i.info("Session ended",{claudeSessionId:t,sessionId:r,reason:e.metadata?.reason});let n=this.activeSessions.get(r);if(n?.mobileEndWatcher&&(n.mobileEndWatcher.stop(),n.mobileEndWatcher=void 0),this.removePortFile(t),n?.waitingForPromptResponse&&(i.info("Clearing prompt wait state - session ending",{sessionId:r}),this.clearPromptState(n)),this.appSyncClient.stopHeartbeat(r),n)try{await this.appSyncClient.updateSession({sessionId:r,status:p.SessionStatus.INACTIVE}),i.info("Session marked as INACTIVE in AppSync",{sessionId:r})}catch(s){i.warn("Failed to update session in AppSync:",s)}else i.warn("Cannot update session - session state not found",{sessionId:r});this.activeSessions.delete(r),this.claudeToBackendSessionId.delete(t),i.debug("Session cleanup completed",{sessionId:r})}subscribeToMobileEvents(e){i.info("Subscribing to mobile events",{sessionId:e});let t=this.activeSessions.get(e);if(!t){i.error("Session not found",{sessionId:e});return}this.appSyncClient.subscribeToEvents(e,async r=>{await this.dispatchMobileEvent(e,r)},r=>{i.error("Subscription error",{sessionId:e,error:r})}),t.subscriptionActive=!0,i.info("Subscription active",{sessionId:e})}async dispatchMobileEvent(e,t){i.info("Received mobile event",{eventId:t.eventId,type:t.type,sessionId:t.sessionId,isEncrypted:t.isEncrypted});let r,n,s=!1,o,a,c;if(t.type===p.EventType.USER_PROMPT||t.type===p.EventType.PROMPT_RESPONSE)if(n=this.activeSessions.get(e),!n)r="no-session";else if(n.processedEventIds?.has(t.eventId))r="skip-dedup";else if(n.inFlightEventIds?.has(t.eventId))r="drop-event-redeliver";else if(n.waitingForPromptResponse){let d=n.promptGenerationToken;if(!d)r="regular";else if(t.type===p.EventType.USER_PROMPT&&n.hasReceivedPromptResponse&&(!t.promptId||t.promptId.length===0))r="drop-stale-answer";else if(t.promptId&&t.promptId.length>0&&d.promptId.length>0&&t.promptId!==d.promptId)r="drop-stale-answer";else{let m=d.promptId.length>0?d.promptId:`__prompt_gen_${d.gen}`;n.inFlightPromptIds?.has(m)?r="drop-in-flight":(n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightEventIds||(n.inFlightEventIds=new Set),n.inFlightPromptIds.add(m),n.inFlightEventIds.add(t.eventId),s=!0,a=m,c=t.eventId,o={promptId:d.promptId,gen:d.gen},r="walker")}}else r="regular";else r="not-user-prompt";let u=t.content||"";if(t.isEncrypted&&this.sessionKey)try{u=p.cryptoService.decryptContent(t.content,this.sessionKey),i.debug("Event decrypted successfully",{eventId:t.eventId})}catch(d){i.error("Failed to decrypt event:",{eventId:t.eventId,error:d}),u=t.content}let g={...t,content:u};if(r!=="skip-dedup")try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:p.DeliveryStatus.DELIVERED}),i.info("Event marked as DELIVERED",{eventId:t.eventId})}catch(d){i.warn("Failed to mark event as DELIVERED",{eventId:t.eventId,error:d})}if(r==="skip-dedup"){i.info("[walker] Subscription-level dedup hit (already processed) \u2014 skipping",{sessionId:e,eventId:t.eventId});return}if(r==="drop-stale-answer"){i.info("[walker] Stale answer dropped \u2014 event.promptId does not match current pending promptId",{sessionId:e,eventId:t.eventId,eventPromptId:t.promptId,currentPromptId:n?.promptGenerationToken?.promptId}),n&&(n.processedEventIds||(n.processedEventIds=new Set),n.processedEventIds.add(t.eventId));try{await this.markEventExecuted(t)}catch(d){i.warn("[walker] markEventExecuted threw on stale-answer drop \u2014 relying on processedEventIds Set",{sessionId:e,eventId:t.eventId,error:String(d)})}return}if(r==="drop-in-flight"){i.warn("[walker] Subscription-level in-flight guard \u2014 dropping duplicate USER_PROMPT (different eventId, same prompt)",{sessionId:e,eventId:t.eventId}),n&&(n.processedEventIds||(n.processedEventIds=new Set),n.processedEventIds.add(t.eventId));try{await this.markEventExecuted(t)}catch(d){i.warn("[walker] markEventExecuted threw on subscription-level duplicate drop \u2014 relying on processedEventIds Set",{sessionId:e,eventId:t.eventId,error:String(d)})}return}if(r==="drop-event-redeliver"){i.info("[walker] Subscription-level event-level redelivery \u2014 silent skip (original still in flight)",{sessionId:e,eventId:t.eventId});return}if(r==="walker"){t.type===p.EventType.PROMPT_RESPONSE&&n&&(n.hasReceivedPromptResponse=!0),await this.handleMobilePromptResponse(e,t,u,n,s,o,a,c);return}if(r==="regular"){if(t.type===p.EventType.PROMPT_RESPONSE){i.warn("Received PROMPT_RESPONSE with no active walker \u2014 dropping",{sessionId:e,eventId:t.eventId,promptId:t.promptId}),n&&(n.processedEventIds||(n.processedEventIds=new Set),n.processedEventIds.add(t.eventId));try{await this.markEventExecuted(t)}catch(d){i.warn("markEventExecuted threw on PROMPT_RESPONSE orphan drop \u2014 relying on processedEventIds Set",{sessionId:e,eventId:t.eventId,error:String(d)})}return}await this.executeMobilePrompt(e,g);return}if(r==="no-session"){i.warn("Received mobile prompt input for unknown session \u2014 ignoring",{sessionId:e,eventId:t.eventId,type:t.type});return}}async handleMobilePromptResponse(e,t,r,n,s=!1,o,a,c){let f=o??n.promptGenerationToken,u=a,g=c;if(!s&&f){let d=f.promptId.length>0?f.promptId:`__prompt_gen_${f.gen}`;if(n.inFlightPromptIds?.has(d)){i.warn("[walker] Duplicate mobile USER_PROMPT for same prompt \u2014 dropping",{sessionId:e,eventId:t.eventId,lockKey:d}),await this.markEventExecutedIdempotent(n,t);return}n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightEventIds||(n.inFlightEventIds=new Set),n.inFlightPromptIds.add(d),n.inFlightEventIds.add(t.eventId),u=d,g=t.eventId}try{if(!s&&n.processedEventIds?.has(t.eventId)){i.info("[walker] Redelivered event already processed \u2014 skipping",{sessionId:e,eventId:t.eventId});return}let d=r.trim(),m=n.pendingPromptId,R=n.pendingSubmitMap,Q=R?Object.keys(R).length:3,v=this.parseInteractivePromptInput(d,Q);i.info("Parsed interactive prompt input",{sessionId:e,content:d,parsed:v,hasSubmitMap:!!R});let l=()=>{let T=n.promptGenerationToken,I=T?.gen,E=f?.gen;return I!==E?(i.warn("[walker] Token mismatch \u2014 external cleanup or new prompt during in-flight handler \u2014 aborting",{sessionId:e,eventId:t.eventId,entryToken:f,currentToken:T}),!0):!1};if(l()){await this.markEventExecutedIdempotent(n,t);return}{let T=n.pendingQuestionsQueue!==void 0,I=d.trim(),E=I.match(/^(\d+)$/);if(T&&n.pendingCurrentQuestion&&E){let y=n.pendingCurrentQuestion.options?.length??0,P=E[1],h=parseInt(P,10),U=!Number.isFinite(h)||h<1||h>y,k=String(h)!==P;if(U||k){let _=this.getWalkerPosition(n);if(i.info("AUQ walker \u2014 bare out-of-range or non-canonical option; routing per dispatch matrix",{sessionId:e,option:P,optionNum:h,realOptionCount:y,isOutOfRange:U,isNonCanonical:k,walkerPosition:_,parsedAction:v.action}),await this.markEventExecutedIdempotent(n,t),l())return;if(_==="on_synth"){let $=await this.promptResponder.answerInteractivePrompt(e,"2",{pressEnter:!1});if(l())return;if(!$){try{await this.emitUserChoice(e,"AskUserQuestion cancel keypress failed (tmux unavailable); your reply was not sent")}catch(A){i.warn("emitUserChoice on on-SYNTH Cancel failed",{sessionId:e,error:A instanceof Error?A.message:String(A)})}return}if(await new Promise(A=>setTimeout(A,1500)),l())return;let F=await this.promptResponder.answerInteractivePrompt(e,I,{pressEnter:!0});if(l())return;F||i.warn("on-SYNTH Cancel followup text-send failed; AUQ cancelled, no new prompt",{sessionId:e});try{await this.emitUserChoice(e,"\u2192 Cancel \u2014 AskUserQuestion cancelled, sending your reply as a new prompt")}catch(A){i.warn("emitUserChoice on on-SYNTH Cancel success failed",{sessionId:e,error:A instanceof Error?A.message:String(A)})}if(l())return;this.promoteFingerprintAndClearPromptState(n);return}await this.handleMobileReplyAsDismissAndPrompt(e,n,I,l);return}}}if(v.action==="select_option"){let T=R?.[v.option]||v.option,I=n.pendingQuestionsQueue!==void 0;i.info("User selected option",{option:v.option,terminalInput:T,isV2AskUserQuestion:I});let E=await this.promptResponder.answerInteractivePrompt(e,T,{pressEnter:!I});if(l()){await this.markEventExecutedIdempotent(n,t);return}if(E){if(await this.markEventExecutedIdempotent(n,t),l())return;if(!m){i.warn("emitAnswerAck called without promptId \u2014 clearing state + skipping ack",{sessionId:e,source:"select_option",eventId:t.eventId}),this.promoteFingerprintAndClearPromptState(n);return}let y=(n.pendingQuestionsQueue?.length??0)===0;try{if(I){let M=parseInt(v.option,10)-1,L=n.pendingCurrentQuestion?.options?.[M],q=typeof L=="string"?L:L&&typeof L=="object"?L.label:`option ${v.option}`,ae=n.pendingCurrentQuestion?._isSubmit===!0,pe=q.toLowerCase(),Y;ae&&pe==="cancel"?Y="\u2192 Cancel \u2014 AskUserQuestion cancelled, no answers submitted":ae&&pe.startsWith("submit")?Y="\u2192 Submit answers \u2014 AskUserQuestion completed":Y=`\u2192 ${q}`,await this.emitUserChoice(e,Y)}else await this.emitAnswerAck(e,`Selected option ${v.option}`,{promptId:m,questionIndex:0,isTerminal:y})}catch(M){i.warn("[walker] user-choice/ack emit failed \u2014 continuing to STEP 7/8",{sessionId:e,promptId:m,isV2AskUserQuestion:I,error:M instanceof Error?M.message:String(M)})}if(l())return;let P=n.pendingQuestionsQueue?.shift();if(P&&(n.pendingCurrentQuestion=P),!P){n.activeAskUserQuestionFingerprint&&(n.completedAskUserQuestionFingerprints||(n.completedAskUserQuestionFingerprints=new Set),n.completedAskUserQuestionFingerprints.add(n.activeAskUserQuestionFingerprint),i.info("AskUserQuestion V2 walker complete \u2014 fingerprint marked closed",{sessionId:e,fingerprint:n.activeAskUserQuestionFingerprint.slice(0,16)})),this.clearPromptState(n);return}let h=`synth-${(0,W.randomUUID)()}`;if(!h){this.promoteFingerprintAndClearPromptState(n),i.warn("Q[next] emit aborted: synthesized promptId was empty; promoted fingerprint + cleared prompt state",{sessionId:e,eventId:t.eventId});return}let U=n.pendingSynthTail??[],k=this.buildQuestionWireData(P,U),_=P.question,$={tool_name:"AskUserQuestion",tool_input:{questions:[P]},options:k.options,submitMap:k.submitMap,instructions:k.instructions},F=this.sessionKey,A=_,j=$,H=!1;F&&(A=p.cryptoService.encryptContent(_,F),j={encrypted:p.cryptoService.encryptMetadata($,F)},H=!0);let G=this.nextPromptGen++,K={promptId:h,gen:G};n.pendingPromptId=h,n.pendingSubmitMap=k.submitMap,n.promptGenerationToken=K;let C=K,N=this.activeSessions.get(e)?.promptGenerationToken;if(!N||N.gen!==C.gen||N.promptId!==C.promptId){i.warn("Q[next] emit aborted: token replaced before await dispatch",{sessionId:e,tokenAtAwait:C,currentToken:N});return}let oe=h.length>0?h:`__prompt_gen_${K.gen}`;n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightPromptIds.add(oe);try{try{await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.INTERACTIVE_PROMPT,source:p.EventSource.DESKTOP,content:A,metadata:j,promptId:h,timestamp:(0,p.prepareEventTimestamp)({orderingKey:e}),isEncrypted:H?!0:void 0}),i.info("Q[next] emit succeeded",{sessionId:e,promptId:h,remaining:n.pendingQuestionsQueue?.length??0})}catch(M){let L=this.activeSessions.get(e),q=L?.promptGenerationToken;q&&q.gen===C.gen&&q.promptId===C.promptId?(this.promoteFingerprintAndClearPromptState(L),i.warn("Q[next] emit failed; promoted fingerprint + cleared prompt state. User must answer remaining questions on desktop terminal.",{sessionId:e,promptId:h,error:M instanceof Error?M.message:String(M)})):i.warn("Q[next] emit failed but a NEW prompt replaced our token during await; not clearing state (would wipe new prompt). Q[next..QN] of the original AskUserQuestion are lost; new prompt continues normally.",{sessionId:e,tokenAtAwait:C,currentToken:q,error:M instanceof Error?M.message:String(M)})}}finally{n.inFlightPromptIds.delete(oe)}}else try{await this.sendPromptError(e,"Failed to select option")}catch(y){i.warn("[walker] sendPromptError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(y)})}finally{await this.markEventExecutedIdempotent(n,t)}}else if(v.action==="option_with_followup"){if(n.pendingQuestionsQueue!==void 0){let y=this.getWalkerPosition(n),P=n.pendingCurrentQuestion?.options?.length??0,h=v.option?parseInt(v.option,10):NaN,U=Number.isFinite(h)&&h>P,k=v.followUpText??"";if(i.info("AUQ walker \u2014 option_with_followup dispatch",{sessionId:e,option:v.option,optionNum:h,explicitCount:P,isSynthDigit:U,walkerPosition:y,followUpTextLen:k.length}),await this.markEventExecutedIdempotent(n,t),l())return;if(U){if(y==="on_synth"){let C=await this.promptResponder.answerInteractivePrompt(e,"2",{pressEnter:!1});if(l())return;if(!C){try{await this.emitUserChoice(e,"AskUserQuestion cancel keypress failed (tmux unavailable); your reply was not sent")}catch(b){i.warn("emitUserChoice on synth-digit on-SYNTH Cancel failed",{sessionId:e,error:b instanceof Error?b.message:String(b)})}return}if(await new Promise(b=>setTimeout(b,1500)),l())return;if(k){let b=await this.promptResponder.answerInteractivePrompt(e,k,{pressEnter:!0});if(l())return;b||i.warn("on-SYNTH Cancel followup text-send failed",{sessionId:e})}try{await this.emitUserChoice(e,"\u2192 Cancel \u2014 AskUserQuestion cancelled, sending your reply as a new prompt")}catch(b){i.warn("emitUserChoice on synth-digit on-SYNTH success failed",{sessionId:e,error:b instanceof Error?b.message:String(b)})}if(l())return;this.promoteFingerprintAndClearPromptState(n);return}await this.handleMobileReplyAsDismissAndPrompt(e,n,k,l);return}let _=R?.[v.option]||v.option,$=h-1,F=n.pendingCurrentQuestion?.options?.[$],A=typeof F=="string"?F:F&&typeof F=="object"?F.label:`option ${v.option}`;if(y==="q1"){let C=await this.promptResponder.answerInteractivePrompt(e,_,{pressEnter:!1});if(l())return;if(!C){try{await this.sendPromptError(e,"Failed to select option. Your reply (including the follow-up text) was not sent. Please retry.")}catch(b){i.warn("sendPromptError on q1 option_with_followup failed",{sessionId:e,error:b instanceof Error?b.message:String(b)})}return}if(await new Promise(b=>setTimeout(b,1500)),l())return;if(k){let b=await this.promptResponder.answerInteractivePrompt(e,k,{pressEnter:!0});if(l())return;b||i.warn("q1 option_with_followup followup text-send failed",{sessionId:e})}try{await this.emitUserChoice(e,k?`\u2192 ${A} + sending your reply as a new prompt`:`\u2192 ${A}`)}catch(b){i.warn("emitUserChoice on q1 option_with_followup success failed",{sessionId:e,error:b instanceof Error?b.message:String(b)})}if(l())return;this.promoteFingerprintAndClearPromptState(n);return}if(y==="nq_mid"){let C=await this.promptResponder.answerInteractivePrompt(e,_,{pressEnter:!1});if(l())return;if(!C){try{await this.sendPromptError(e,"Failed to commit Q[i]. Your reply was not sent. Please retry.")}catch(N){i.warn("sendPromptError on nq_mid option_with_followup failed",{sessionId:e,error:N instanceof Error?N.message:String(N)})}return}if(await new Promise(N=>setTimeout(N,200)),l())return;let b=n.pendingQuestionsQueue?.shift();b?n.pendingCurrentQuestion=b:i.warn("nq_mid option_with_followup: queue.shift returned undefined unexpectedly",{sessionId:e}),await this.handleMobileReplyAsDismissAndPrompt(e,n,k,l);return}if(y==="nq_last"){await this.handleOptionWithFollowupOnLastQ(e,n,_,A,k,l);return}let j=await this.promptResponder.answerInteractivePrompt(e,_,{pressEnter:!1});if(l())return;if(!j){try{await this.sendPromptError(e,"Failed to send SYNTH keypress. Your reply was not sent. Please retry.")}catch(C){i.warn("sendPromptError on on-SYNTH option_with_followup failed",{sessionId:e,error:C instanceof Error?C.message:String(C)})}return}if(await new Promise(C=>setTimeout(C,1500)),l())return;if(k){let C=await this.promptResponder.answerInteractivePrompt(e,k,{pressEnter:!0});if(l())return;C||i.warn("on-SYNTH option_with_followup text-send failed",{sessionId:e})}let H=n.pendingCurrentQuestion?._isSubmit===!0,G=A.toLowerCase(),K;H&&G==="cancel"?K=k?"\u2192 Cancel \u2014 AskUserQuestion cancelled, sending your reply as a new prompt":"\u2192 Cancel \u2014 AskUserQuestion cancelled, no answers submitted":H&&G.startsWith("submit")?K=k?"\u2192 Submit answers \u2014 AskUserQuestion completed, sending your reply as a new prompt":"\u2192 Submit answers \u2014 AskUserQuestion completed":K=k?`\u2192 ${A} + sending your reply as a new prompt`:`\u2192 ${A}`;try{await this.emitUserChoice(e,K)}catch(C){i.warn("emitUserChoice on on-SYNTH option_with_followup success failed",{sessionId:e,error:C instanceof Error?C.message:String(C)})}if(l())return;this.promoteFingerprintAndClearPromptState(n);return}let I=R?.[v.option]||v.option;i.info("User selected option with follow-up",{option:v.option,terminalInput:I,followUpText:v.followUpText});let E=await this.promptResponder.answerInteractivePrompt(e,I);if(l()){await this.markEventExecutedIdempotent(n,t);return}if(E){if(await this.markEventExecutedIdempotent(n,t),l())return;if(!m){i.warn("emitAnswerAck called without promptId \u2014 clearing state + skipping ack",{sessionId:e,source:"option_with_followup",eventId:t.eventId}),this.promoteFingerprintAndClearPromptState(n);return}try{await this.emitAnswerAck(e,`Selected option ${v.option}`,{promptId:m,questionIndex:0,isTerminal:!0})}catch(y){i.warn("[walker] emitAnswerAck (option_with_followup) failed \u2014 continuing to clearPromptState + executeMobilePrompt",{sessionId:e,promptId:m,error:y instanceof Error?y.message:String(y)})}if(l())return;if(this.promoteFingerprintAndClearPromptState(n),v.followUpText){await new Promise(P=>setTimeout(P,1e3));let y={...t,content:v.followUpText};await this.executeMobilePrompt(e,y)}}else try{await this.sendPromptError(e,"Failed to select option. Your reply (including the follow-up text) was not sent. Please retry.")}catch(y){i.warn("[walker] sendPromptError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(y)})}finally{await this.markEventExecutedIdempotent(n,t)}}else{if(n.pendingQuestionsQueue!==void 0){let E=this.getWalkerPosition(n);if(i.info("AUQ walker \u2014 send_as_response dispatch",{sessionId:e,walkerPosition:E,contentPreview:d.slice(0,80)}),await this.markEventExecutedIdempotent(n,t),l())return;if(E==="on_synth"){let y=await this.promptResponder.answerInteractivePrompt(e,"2",{pressEnter:!1});if(l())return;if(!y){try{await this.emitUserChoice(e,"AskUserQuestion cancel keypress failed (tmux unavailable); your reply was not sent")}catch(h){i.warn("emitUserChoice on send_as_response on-SYNTH Cancel failed",{sessionId:e,error:h instanceof Error?h.message:String(h)})}return}if(await new Promise(h=>setTimeout(h,1500)),l())return;let P=await this.promptResponder.answerInteractivePrompt(e,d,{pressEnter:!0});if(l())return;P||i.warn("send_as_response on-SYNTH text-send failed",{sessionId:e});try{await this.emitUserChoice(e,"\u2192 Cancel \u2014 AskUserQuestion cancelled, sending your reply as a new prompt")}catch(h){i.warn("emitUserChoice on send_as_response on-SYNTH success failed",{sessionId:e,error:h instanceof Error?h.message:String(h)})}if(l())return;this.promoteFingerprintAndClearPromptState(n);return}await this.handleMobileReplyAsDismissAndPrompt(e,n,d,l);return}i.info("Sending as free-form response to interactive prompt",{response:d});let I=await this.promptResponder.answerInteractivePrompt(e,d);if(l()){await this.markEventExecutedIdempotent(n,t);return}if(I){if(await this.markEventExecutedIdempotent(n,t),l())return;if(!m){i.warn("emitAnswerAck called without promptId \u2014 clearing state + skipping ack",{sessionId:e,source:"send_as_response",eventId:t.eventId}),this.promoteFingerprintAndClearPromptState(n);return}try{await this.emitAnswerAck(e,"Response sent to interactive prompt",{promptId:m,questionIndex:0,isTerminal:!0})}catch(E){i.warn("[walker] emitAnswerAck (send_as_response) failed \u2014 continuing to clearPromptState",{sessionId:e,promptId:m,error:E instanceof Error?E.message:String(E)})}if(l())return;this.promoteFingerprintAndClearPromptState(n)}else try{await this.sendPromptError(e,"Failed to send response")}catch(E){i.warn("[walker] sendPromptError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(E)})}finally{await this.markEventExecutedIdempotent(n,t)}}}finally{u&&n.inFlightPromptIds&&n.inFlightPromptIds.delete(u),g&&n.inFlightEventIds&&n.inFlightEventIds.delete(g)}}async sendInteractivePromptAsync(e,t,r){let n=this.activeSessions.get(e),s=n?.promptGenerationToken?{...n.promptGenerationToken}:void 0,o=(0,p.prepareEventTimestamp)({orderingKey:e});await new Promise(l=>setTimeout(l,500));let a=process.env.CODEVIBE_TMUX_SESSION,c={...t.metadata||{}},f=t.metadata?.tool_name,u=t.metadata?.tool_input,g=f==="AskUserQuestion"&&Array.isArray(u?.questions)?u.questions:[];if(g.length>0&&Array.isArray(g[0]?.options)&&g[0].options.length>0){let l=g[0],T=[];if(a)try{let{exec:h}=await import("child_process"),U=_=>new Promise(($,F)=>{h(_,{timeout:5e3},(A,j)=>{A?F(A):$({stdout:j||""})})}),{stdout:k}=await U(`tmux capture-pane -p -e -S -30 -t '${a}'`);T=this.parseAskUserQuestionSynthTail(k),i.info("AskUserQuestion synth-tail parsed from tmux",{tailCount:T.length,tail:T.map(_=>_.label)})}catch(h){i.warn("Failed to capture tmux for AskUserQuestion synth-tail; emitting without synth tail",{error:h instanceof Error?h.message:String(h)})}else i.info("No tmux session \u2014 AskUserQuestion synth tail will be empty");let I=this.activeSessions.get(e);if(I){let h=I.promptGenerationToken;s&&h?.gen===s.gen?I.pendingSynthTail=T:i.warn("AskUserQuestion synth-tail: stale async \u2014 token gen mismatch, skipping pendingSynthTail write",{tokenAtEmit:s,currentToken:h,sessionId:e})}let E=this.buildQuestionWireData(l,T);c.options=JSON.parse(JSON.stringify(E.options)),c.submitMap=JSON.parse(JSON.stringify(E.submitMap)),c.instructions=E.instructions,c.tool_name="AskUserQuestion",c.tool_input={questions:[l]},r=l.question;let y=typeof t.prompt_id=="string"&&t.prompt_id.length>0,P=Me(g);if(y){let h=this.activeSessions.get(e);if(h){let U=h.promptGenerationToken;s&&U?.gen===s.gen?(h.pendingQuestionsQueue=P.remainingQueue,h.pendingCurrentQuestion=l,h.hasReviewScreen=P.hasReviewScreen):i.warn("AskUserQuestion: stale async \u2014 token gen mismatch, skipping walker-state write",{tokenAtEmit:s,currentToken:U,sessionId:e})}}else i.warn("AskUserQuestion: empty prompt_id, degrading to single-Q legacy emit",{questionCount:g.length});i.info("AskUserQuestion: emitting Q1 only (Q2..QN queued)",{questionCount:P.questionCount,hasReviewScreen:P.hasReviewScreen,queuedRemaining:y?P.remainingQueue.length:0,optionCountFirst:E.options.length,questionPreview:l.question.slice(0,80)})}else if(a)try{let{exec:l}=await import("child_process"),T=P=>new Promise((h,U)=>{l(P,{timeout:5e3},(k,_)=>{k?U(k):h({stdout:_||""})})}),{stdout:I}=await T(`tmux capture-pane -p -e -S -30 -t '${a}'`),E=I.split(`
6
- `);i.info("tmux capture result",{tmuxSession:a,totalLines:E.length,lastLines:E.slice(-15).map(P=>P.replace(/\x1B[^m]*m/g,"").trim()).filter(Boolean)});let y=(0,p.parseInteractivePrompt)(I);y&&y.options.length>0?(c.options=y.options,c.submitMap=y.submitMap,c.instructions=this.buildPromptInstructions(y),i.info("Parsed dynamic options from tmux",{optionCount:y.options.length,kind:y.kind,options:y.options})):(i.info("No dynamic options parsed from tmux, using fallback",{parsedResult:y}),this.addFallbackOptions(c))}catch(l){i.warn("Failed to capture tmux pane for options",{error:l}),this.addFallbackOptions(c)}else i.warn("No tmux session \u2014 using fallback options"),this.addFallbackOptions(c);let d=this.activeSessions.get(e);if(d&&c.submitMap){let l=d.promptGenerationToken;s&&l?.gen===s.gen?d.pendingSubmitMap=c.submitMap:i.warn("Interactive prompt async: stale async \u2014 token gen mismatch, skipping pendingSubmitMap write",{tokenAtEmit:s,currentToken:l,sessionId:e})}let m=r,x=c,R=!1;this.sessionKey&&(m=p.cryptoService.encryptContent(r,this.sessionKey),x={encrypted:p.cryptoService.encryptMetadata(x,this.sessionKey)},R=!0);let v=this.activeSessions.get(e)?.promptGenerationToken;if(s&&v?.gen!==s.gen){i.warn("Interactive prompt emit: stale token \u2014 newer INTERACTIVE_PROMPT replaced ours; skipping AppSync emit",{sessionId:e,tokenAtEmit:s,currentToken:v});return}await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.INTERACTIVE_PROMPT,source:t.source,content:m,metadata:x,promptId:t.prompt_id,timestamp:o,isEncrypted:R?!0:void 0}),i.info("Interactive prompt sent to AppSync with dynamic options",{sessionId:e})}buildQuestionWireData(e,t=[]){let r=(e.options||[]).map((c,f)=>{let u=typeof c=="string",g=u?c:c.label||"",d=u?"":c.description||"",m=u?"":c.preview||"",x={number:String(f+1),text:g};return d&&(x.description=d),m&&(x.preview=m),x});if(!e._isSubmit)for(let c of t)r.push({number:String(r.length+1),text:c.label});let n=Object.fromEntries(r.map(c=>[c.number,c.number])),s=(e.options||[]).length,o=t.findIndex(c=>c.label==="Type something"),a;if(e._isSubmit)a="Reply with 1 to submit your answers or 2 to cancel.";else if(e.multiSelect)a=`Reply with comma-separated numbers (e.g., 1,3) for "${e.header||e.question}"`;else if(o>=0){let c=String(s+o+1);a=`Reply with the number of your choice. For option ${c} (Type something), reply "${c}, your answer".`}else a="Reply with the number of your choice.";return{options:r,submitMap:n,instructions:a}}parseAskUserQuestionSynthTail(e){let t=(0,p.normalizeSnapshot)(e);if(!t)return[];let r=t.split(`
7
- `).slice(-14),n=[],s=new Set,o=/^\s*(?:[›❯▸▶➜➤]\s*)?(?:\d+\.\s+)?(Type something|Chat about this)\.?\s*$/i;for(let a of r){let c=a.match(o);if(!c)continue;let u=c[1].toLowerCase()==="type something"?"Type something":"Chat about this";s.has(u)||(n.push({label:u}),s.add(u))}return n}addFallbackOptions(e){e.options=[{number:"1",text:"Yes"},{number:"2",text:"Yes, and don't ask again"},{number:"3",text:"Reject and tell Claude what to do differently"}],e.submitMap={1:"1",2:"2",3:"3"},e.instructions="Reply with 1, 2, or 3. Append a message to provide alternative instructions."}buildPromptInstructions(e){return`Reply with ${e.options.map(r=>r.number).join(", ")}. Append a message to provide alternative instructions.`}parseInteractivePromptInput(e,t=3){return Pe(e,t)}async markEventExecuted(e){try{await this.appSyncClient.updateEventStatus({eventId:e.eventId,sessionId:e.sessionId,timestamp:e.timestamp,deliveryStatus:p.DeliveryStatus.EXECUTED}),i.info("Event marked as EXECUTED",{eventId:e.eventId})}catch(t){i.warn("Failed to mark event as EXECUTED",{eventId:e.eventId,error:t})}}async sendPromptError(e,t){let r={error:!0},n=t,s=r,o=!1;this.sessionKey&&(n=p.cryptoService.encryptContent(t,this.sessionKey),s={encrypted:p.cryptoService.encryptMetadata(r,this.sessionKey)},o=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:n,metadata:s,timestamp:(0,p.prepareEventTimestamp)({orderingKey:e}),isEncrypted:o?!0:void 0})}async emitUserChoice(e,t){let r=t,n={source:"codevibe_v2_user_choice"},s=!1;this.sessionKey&&(r=p.cryptoService.encryptContent(t,this.sessionKey),n={encrypted:p.cryptoService.encryptMetadata({source:"codevibe_v2_user_choice"},this.sessionKey)},s=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.ASSISTANT_RESPONSE,source:p.EventSource.DESKTOP,content:r,metadata:n,timestamp:(0,p.prepareEventTimestamp)({orderingKey:e}),isEncrypted:s?!0:void 0})}async emitAnswerAck(e,t,r){let n={promptAnswered:!0,...r},s=t,o=n,a=!1;this.sessionKey&&(s=p.cryptoService.encryptContent(t,this.sessionKey),o={encrypted:p.cryptoService.encryptMetadata(n,this.sessionKey)},a=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:s,metadata:o,timestamp:(0,p.prepareEventTimestamp)({orderingKey:e}),isEncrypted:a?!0:void 0})}promoteFingerprintAndClearPromptState(e){e.activeAskUserQuestionFingerprint&&(e.completedAskUserQuestionFingerprints||(e.completedAskUserQuestionFingerprints=new Set),e.completedAskUserQuestionFingerprints.add(e.activeAskUserQuestionFingerprint)),this.clearPromptState(e)}clearPromptState(e){e.waitingForPromptResponse=!1,e.pendingPromptId=void 0,e.pendingSubmitMap=void 0,e.pendingQuestionsQueue=void 0,e.pendingCurrentQuestion=void 0,e.pendingSynthTail=void 0,e.hasReviewScreen=void 0,e.activeAskUserQuestionFingerprint=void 0,e.promptGenerationToken=void 0}async detectWalkerActive(e){let t=process.env.CODEVIBE_TMUX_SESSION;if(!t)return i.debug("detectWalkerActive: no CODEVIBE_TMUX_SESSION env \u2014 conservatively assuming walker active",{sessionId:e}),!0;try{let{exec:r}=await import("child_process"),n=l=>new Promise((T,I)=>{r(l,{timeout:3e3},(E,y)=>{E?I(E):T({stdout:y||""})})}),{stdout:s}=await n(`tmux capture-pane -p -e -S -30 -t '${t}'`),o=/\x1B(?:\[[?!0-9;]*[A-Za-z]|\][^\x07\x1B]*(?:\x07|\x1B\\)|[()][A-B012])/g,a=s.split(`
8
- `).map(l=>l.replace(o,"").trimEnd()),c=a.length-1;for(;c>=0&&a[c].trim()==="";)c-=1;if(c<0)return i.warn("detectWalkerActive: pane empty after ANSI strip \u2014 conservatively assuming walker active",{sessionId:e}),!0;let f=/(?:Esc to cancel|Enter to select|↑\/↓ to navigate|Tab to switch|shift\+tab|Chat about this|Type something|Notes: press|\(esc\)|\(shift\+tab\))/i,u=/^\s*(?:[›❯▸▶➜➤]\s*)?\d+\.\s+/,g=-1;for(let l=c;l>=0;l-=1)if(u.test(a[l])){g=l;break}let d=g>=0,m=-1;for(let l=c;l>=0;l-=1)if(f.test(a[l])){m=l;break}let x=m<0?1/0:c-m,R=m>=0&&x<=3,Q=m>=0&&g>=0&&m>=g&&m-g<=5,v=d&&R&&Q;return i.info("detectWalkerActive",{sessionId:e,walkerActive:v,hasParserBlock:d,chromeNearBottom:R,chromeFollowsBlock:Q,chromeDistanceFromBottom:Number.isFinite(x)?x:-1,parserBlockEndsAt:g,lastNonBlank:c,lastChromeLineAt:m,lastLineSample:a[c]?.slice(0,80)??""}),v}catch(r){return i.warn("detectWalkerActive: tmux capture/parse failed \u2014 conservatively assuming walker active",{sessionId:e,error:r instanceof Error?r.message:String(r)}),!0}}getWalkerPosition(e){return e.pendingCurrentQuestion?._isSubmit?"on_synth":e.hasReviewScreen?(e.pendingQuestionsQueue?.length??0)>=2?"nq_mid":"nq_last":"q1"}async emitDegradedAUQNotification(e,t){try{let r=t,n={source:"codevibe_auq_degraded"},s=!1;this.sessionKey&&(r=p.cryptoService.encryptContent(t,this.sessionKey),n={encrypted:p.cryptoService.encryptMetadata({source:"codevibe_auq_degraded"},this.sessionKey)},s=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.ASSISTANT_RESPONSE,source:p.EventSource.DESKTOP,content:r,metadata:n,isEncrypted:s?!0:void 0}),i.info("Emitted degraded-AUQ notification",{sessionId:e,textPreview:t.slice(0,60)})}catch(r){i.warn("emitDegradedAUQNotification failed",{sessionId:e,error:r instanceof Error?r.message:String(r)})}}async handleMobileReplyAsDismissAndPrompt(e,t,r,n){let s=await this.promptResponder.sendKey(e,"Escape");if(n())return!1;if(!s){try{await this.emitUserChoice(e,"AskUserQuestion dismissal failed (tmux unavailable)")}catch(a){i.warn("emitUserChoice on dismiss-ok1-failed failed",{sessionId:e,error:a instanceof Error?a.message:String(a)})}return!1}if(await new Promise(a=>setTimeout(a,200)),n())return!1;let o=await this.promptResponder.answerInteractivePrompt(e,r,{pressEnter:!0});if(n())return!1;if(!o){try{await this.emitUserChoice(e,"AskUserQuestion dismissed but text-send failed; please retry")}catch(a){i.warn("emitUserChoice on dismiss-ok2-failed failed",{sessionId:e,error:a instanceof Error?a.message:String(a)})}return n()||this.promoteFingerprintAndClearPromptState(t),!1}try{await this.emitUserChoice(e,"\u2192 AskUserQuestion dismissed, sending your reply as a new prompt")}catch(a){i.warn("emitUserChoice on dismiss-success failed \u2014 continuing to promote+clear",{sessionId:e,error:a instanceof Error?a.message:String(a)})}return n()?(i.warn("handleMobileReplyAsDismissAndPrompt: aborted post-emitUserChoice (token rotated); leaving session state for new prompt",{sessionId:e}),!1):(this.promoteFingerprintAndClearPromptState(t),!0)}async handleOptionWithFollowupOnLastQ(e,t,r,n,s,o){let a=await this.promptResponder.answerInteractivePrompt(e,r,{pressEnter:!1});if(o())return!1;if(!a){try{await this.emitUserChoice(e,"Failed to commit Q[N] (tmux unavailable); your reply was not sent")}catch(u){i.warn("emitUserChoice on lastQ-ok1-failed failed",{sessionId:e,error:u instanceof Error?u.message:String(u)})}return!1}if(await new Promise(u=>setTimeout(u,200)),o())return!1;let c=await this.promptResponder.answerInteractivePrompt(e,"1",{pressEnter:!1});if(o())return!1;if(!c){try{await this.emitUserChoice(e,"Q[N] committed but Submit failed (tmux unavailable); please retry on desktop")}catch(u){i.warn("emitUserChoice on lastQ-ok2-failed failed",{sessionId:e,error:u instanceof Error?u.message:String(u)})}return o()||this.promoteFingerprintAndClearPromptState(t),!1}if(await new Promise(u=>setTimeout(u,1500)),o())return!1;let f=await this.promptResponder.answerInteractivePrompt(e,s,{pressEnter:!0});if(o())return!1;f||i.warn("handleOptionWithFollowupOnLastQ: followup text send failed",{sessionId:e});try{await this.emitUserChoice(e,`\u2192 ${n} + sending your reply as a new prompt`)}catch(u){i.warn("emitUserChoice on lastQ-success failed \u2014 continuing to promote+clear",{sessionId:e,error:u instanceof Error?u.message:String(u)})}return o()?(i.warn("handleOptionWithFollowupOnLastQ: aborted post-emitUserChoice (token rotated); leaving session state for new prompt",{sessionId:e}),!1):(this.promoteFingerprintAndClearPromptState(t),!0)}computeAskUserQuestionFingerprint(e){if(!(!e||typeof e!="object"))try{let t=this.stringifyCanonical(e);return(0,W.createHash)("sha256").update(t).digest("hex")}catch(t){i.warn("Failed to fingerprint AskUserQuestion questions",{error:t instanceof Error?t.message:String(t)});return}}stringifyCanonical(e){return e===null||typeof e!="object"?JSON.stringify(e):Array.isArray(e)?"["+e.map(r=>this.stringifyCanonical(r)).join(",")+"]":"{"+Object.keys(e).sort().map(r=>JSON.stringify(r)+":"+this.stringifyCanonical(e[r])).join(",")+"}"}async markEventExecutedIdempotent(e,t){e.processedEventIds||(e.processedEventIds=new Set),e.processedEventIds.add(t.eventId);try{await this.markEventExecuted(t)}catch(r){i.warn("[walker] markEventExecuted threw \u2014 relying on processedEventIds set for dedup",{sessionId:t.sessionId,eventId:t.eventId,error:String(r)})}}isSessionLimitExceeded(e){return this.getErrorMessage(e).includes("SESSION_LIMIT_EXCEEDED")}isUsageLimitExceeded(e){let t=this.getErrorMessage(e);return t.includes("MESSAGE_LIMIT_EXCEEDED")||t.includes("IMAGE_LIMIT_EXCEEDED")}getErrorMessage(e){if(e instanceof Error)return e.message;if(typeof e=="object"&&e!==null){let t=e;if(t.errors&&Array.isArray(t.errors))return t.errors.map(r=>r.message||"").join(" ");if(typeof t.message=="string")return t.message}return String(e)}displaySubscriptionLimitError(e,t){let r=this.getErrorMessage(e),n="",s=r.match(/for your (\w+) plan/i);s&&(n=` (${s[1]} tier)`);let o="",a=r.match(/of (\d+)/);switch(a&&(o=` [Limit: ${a[1]}]`),console.log(`
5
+ `)}}catch(h){if(this.isSessionLimitExceeded(h)){this.displaySubscriptionLimitError(h,"session"),this.activeSessions.delete(r),this.removePortFile(t);return}i.error("Failed to create/resume session:",h)}this.subscribeToMobileEvents(r),this.appSyncClient.startHeartbeat(r);let c=this.activeSessions.get(r);c&&(c.mobileEndWatcher=this.appSyncClient.watchForMobileEnd(r,async()=>{i.info("Mobile ended session \u2014 sending desktop quit",{sessionId:r});let h=process.env[we];if(!h){i.warn("No tmux session set; skipping desktop self-terminate",{sessionId:r,expectedEnv:we});return}await Ue(h,Re)}))}async handleSessionEnd(e){let t=e.session_id,r=this.claudeToBackendSessionId.get(t)||this.generateBackendSessionId(t);i.info("Session ended",{claudeSessionId:t,sessionId:r,reason:e.metadata?.reason});let n=this.activeSessions.get(r);if(n?.mobileEndWatcher&&(n.mobileEndWatcher.stop(),n.mobileEndWatcher=void 0),this.removePortFile(t),n?.waitingForPromptResponse&&(i.info("Clearing prompt wait state - session ending",{sessionId:r}),this.clearPromptState(n)),this.appSyncClient.stopHeartbeat(r),n)try{await this.appSyncClient.updateSession({sessionId:r,status:p.SessionStatus.INACTIVE}),i.info("Session marked as INACTIVE in AppSync",{sessionId:r})}catch(s){i.warn("Failed to update session in AppSync:",s)}else i.warn("Cannot update session - session state not found",{sessionId:r});this.activeSessions.delete(r),this.claudeToBackendSessionId.delete(t),i.debug("Session cleanup completed",{sessionId:r})}subscribeToMobileEvents(e){i.info("Subscribing to mobile events",{sessionId:e});let t=this.activeSessions.get(e);if(!t){i.error("Session not found",{sessionId:e});return}this.appSyncClient.subscribeToEvents(e,async r=>{await this.dispatchMobileEvent(e,r)},r=>{i.error("Subscription error",{sessionId:e,error:r})}),t.subscriptionActive=!0,i.info("Subscription active",{sessionId:e})}async dispatchMobileEvent(e,t){i.info("Received mobile event",{eventId:t.eventId,type:t.type,sessionId:t.sessionId,isEncrypted:t.isEncrypted});let r,n,s=!1,o,a,c;if(t.type===p.EventType.USER_PROMPT||t.type===p.EventType.PROMPT_RESPONSE)if(n=this.activeSessions.get(e),!n)r="no-session";else if(n.processedEventIds?.has(t.eventId))r="skip-dedup";else if(n.inFlightEventIds?.has(t.eventId))r="drop-event-redeliver";else if(n.waitingForPromptResponse){let d=n.promptGenerationToken;if(!d)r="regular";else if(t.type===p.EventType.USER_PROMPT&&n.hasReceivedPromptResponse&&(!t.promptId||t.promptId.length===0))r="drop-stale-answer";else if(t.promptId&&t.promptId.length>0&&d.promptId.length>0&&t.promptId!==d.promptId)r="drop-stale-answer";else{let m=d.promptId.length>0?d.promptId:`__prompt_gen_${d.gen}`;n.inFlightPromptIds?.has(m)?r="drop-in-flight":(n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightEventIds||(n.inFlightEventIds=new Set),n.inFlightPromptIds.add(m),n.inFlightEventIds.add(t.eventId),s=!0,a=m,c=t.eventId,o={promptId:d.promptId,gen:d.gen},r="walker")}}else r="regular";else r="not-user-prompt";let u=t.content||"";if(t.isEncrypted&&this.sessionKey)try{u=p.cryptoService.decryptContent(t.content,this.sessionKey),i.debug("Event decrypted successfully",{eventId:t.eventId})}catch(d){i.error("Failed to decrypt event:",{eventId:t.eventId,error:d}),u=t.content}let g={...t,content:u};if(r!=="skip-dedup")try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:p.DeliveryStatus.DELIVERED}),i.info("Event marked as DELIVERED",{eventId:t.eventId})}catch(d){i.warn("Failed to mark event as DELIVERED",{eventId:t.eventId,error:d})}if(r==="skip-dedup"){i.info("[walker] Subscription-level dedup hit (already processed) \u2014 skipping",{sessionId:e,eventId:t.eventId});return}if(r==="drop-stale-answer"){i.info("[walker] Stale answer dropped \u2014 event.promptId does not match current pending promptId",{sessionId:e,eventId:t.eventId,eventPromptId:t.promptId,currentPromptId:n?.promptGenerationToken?.promptId}),n&&(n.processedEventIds||(n.processedEventIds=new Set),n.processedEventIds.add(t.eventId));try{await this.markEventExecuted(t)}catch(d){i.warn("[walker] markEventExecuted threw on stale-answer drop \u2014 relying on processedEventIds Set",{sessionId:e,eventId:t.eventId,error:String(d)})}return}if(r==="drop-in-flight"){i.warn("[walker] Subscription-level in-flight guard \u2014 dropping duplicate USER_PROMPT (different eventId, same prompt)",{sessionId:e,eventId:t.eventId}),n&&(n.processedEventIds||(n.processedEventIds=new Set),n.processedEventIds.add(t.eventId));try{await this.markEventExecuted(t)}catch(d){i.warn("[walker] markEventExecuted threw on subscription-level duplicate drop \u2014 relying on processedEventIds Set",{sessionId:e,eventId:t.eventId,error:String(d)})}return}if(r==="drop-event-redeliver"){i.info("[walker] Subscription-level event-level redelivery \u2014 silent skip (original still in flight)",{sessionId:e,eventId:t.eventId});return}if(r==="walker"){t.type===p.EventType.PROMPT_RESPONSE&&n&&(n.hasReceivedPromptResponse=!0),await this.handleMobilePromptResponse(e,t,u,n,s,o,a,c);return}if(r==="regular"){if(t.type===p.EventType.PROMPT_RESPONSE){i.warn("Received PROMPT_RESPONSE with no active walker \u2014 dropping",{sessionId:e,eventId:t.eventId,promptId:t.promptId}),n&&(n.processedEventIds||(n.processedEventIds=new Set),n.processedEventIds.add(t.eventId));try{await this.markEventExecuted(t)}catch(d){i.warn("markEventExecuted threw on PROMPT_RESPONSE orphan drop \u2014 relying on processedEventIds Set",{sessionId:e,eventId:t.eventId,error:String(d)})}return}await this.executeMobilePrompt(e,g);return}if(r==="no-session"){i.warn("Received mobile prompt input for unknown session \u2014 ignoring",{sessionId:e,eventId:t.eventId,type:t.type});return}}async handleMobilePromptResponse(e,t,r,n,s=!1,o,a,c){let h=o??n.promptGenerationToken,u=a,g=c;if(!s&&h){let d=h.promptId.length>0?h.promptId:`__prompt_gen_${h.gen}`;if(n.inFlightPromptIds?.has(d)){i.warn("[walker] Duplicate mobile USER_PROMPT for same prompt \u2014 dropping",{sessionId:e,eventId:t.eventId,lockKey:d}),await this.markEventExecutedIdempotent(n,t);return}n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightEventIds||(n.inFlightEventIds=new Set),n.inFlightPromptIds.add(d),n.inFlightEventIds.add(t.eventId),u=d,g=t.eventId}try{if(!s&&n.processedEventIds?.has(t.eventId)){i.info("[walker] Redelivered event already processed \u2014 skipping",{sessionId:e,eventId:t.eventId});return}let d=r.trim(),m=n.pendingPromptId,R=n.pendingSubmitMap,Q=R?Object.keys(R).length:3,v=this.parseInteractivePromptInput(d,Q);i.info("Parsed interactive prompt input",{sessionId:e,content:d,parsed:v,hasSubmitMap:!!R});let l=()=>{let T=n.promptGenerationToken,I=T?.gen,E=h?.gen;return I!==E?(i.warn("[walker] Token mismatch \u2014 external cleanup or new prompt during in-flight handler \u2014 aborting",{sessionId:e,eventId:t.eventId,entryToken:h,currentToken:T}),!0):!1};if(l()){await this.markEventExecutedIdempotent(n,t);return}{let T=n.pendingQuestionsQueue!==void 0,I=d.trim(),E=I.match(/^(\d+)$/);if(T&&n.pendingCurrentQuestion&&E){let y=n.pendingCurrentQuestion.options?.length??0,P=E[1],f=parseInt(P,10),U=!Number.isFinite(f)||f<1||f>y,k=String(f)!==P;if(U||k){let _=this.getWalkerPosition(n);if(i.info("AUQ walker \u2014 bare out-of-range or non-canonical option; routing per dispatch matrix",{sessionId:e,option:P,optionNum:f,realOptionCount:y,isOutOfRange:U,isNonCanonical:k,walkerPosition:_,parsedAction:v.action}),await this.markEventExecutedIdempotent(n,t),l())return;if(_==="on_synth"){let $=await this.promptResponder.answerInteractivePrompt(e,"2",{pressEnter:!1});if(l())return;if(!$){try{await this.emitUserChoice(e,"AskUserQuestion cancel keypress failed (tmux unavailable); your reply was not sent")}catch(A){i.warn("emitUserChoice on on-SYNTH Cancel failed",{sessionId:e,error:A instanceof Error?A.message:String(A)})}return}if(await new Promise(A=>setTimeout(A,1500)),l())return;let F=await this.promptResponder.answerInteractivePrompt(e,I,{pressEnter:!0});if(l())return;F||i.warn("on-SYNTH Cancel followup text-send failed; AUQ cancelled, no new prompt",{sessionId:e});try{await this.emitUserChoice(e,"\u2192 Cancel \u2014 AskUserQuestion cancelled, sending your reply as a new prompt")}catch(A){i.warn("emitUserChoice on on-SYNTH Cancel success failed",{sessionId:e,error:A instanceof Error?A.message:String(A)})}if(l())return;this.promoteFingerprintAndClearPromptState(n);return}await this.handleMobileReplyAsDismissAndPrompt(e,n,I,l);return}}}if(v.action==="select_option"){let T=R?.[v.option]||v.option,I=n.pendingQuestionsQueue!==void 0;i.info("User selected option",{option:v.option,terminalInput:T,isV2AskUserQuestion:I});let E=await this.promptResponder.answerInteractivePrompt(e,T,{pressEnter:!I});if(l()){await this.markEventExecutedIdempotent(n,t);return}if(E){if(await this.markEventExecutedIdempotent(n,t),l())return;if(!m){i.warn("emitAnswerAck called without promptId \u2014 clearing state + skipping ack",{sessionId:e,source:"select_option",eventId:t.eventId}),this.promoteFingerprintAndClearPromptState(n);return}let y=(n.pendingQuestionsQueue?.length??0)===0;try{if(I){let M=parseInt(v.option,10)-1,L=n.pendingCurrentQuestion?.options?.[M],q=typeof L=="string"?L:L&&typeof L=="object"?L.label:`option ${v.option}`,ae=n.pendingCurrentQuestion?._isSubmit===!0,pe=q.toLowerCase(),Y;ae&&pe==="cancel"?Y="\u2192 Cancel \u2014 AskUserQuestion cancelled, no answers submitted":ae&&pe.startsWith("submit")?Y="\u2192 Submit answers \u2014 AskUserQuestion completed":Y=`\u2192 ${q}`,await this.emitUserChoice(e,Y)}else await this.emitAnswerAck(e,`Selected option ${v.option}`,{promptId:m,questionIndex:0,isTerminal:y})}catch(M){i.warn("[walker] user-choice/ack emit failed \u2014 continuing to STEP 7/8",{sessionId:e,promptId:m,isV2AskUserQuestion:I,error:M instanceof Error?M.message:String(M)})}if(l())return;let P=n.pendingQuestionsQueue?.shift();if(P&&(n.pendingCurrentQuestion=P),!P){n.activeAskUserQuestionFingerprint&&(n.completedAskUserQuestionFingerprints||(n.completedAskUserQuestionFingerprints=new Set),n.completedAskUserQuestionFingerprints.add(n.activeAskUserQuestionFingerprint),i.info("AskUserQuestion V2 walker complete \u2014 fingerprint marked closed",{sessionId:e,fingerprint:n.activeAskUserQuestionFingerprint.slice(0,16)})),this.clearPromptState(n);return}let f=`synth-${(0,W.randomUUID)()}`;if(!f){this.promoteFingerprintAndClearPromptState(n),i.warn("Q[next] emit aborted: synthesized promptId was empty; promoted fingerprint + cleared prompt state",{sessionId:e,eventId:t.eventId});return}let U=n.pendingSynthTail??[],k=this.buildQuestionWireData(P,U),_=P.question,$={tool_name:"AskUserQuestion",tool_input:{questions:[P]},options:k.options,submitMap:k.submitMap,instructions:k.instructions},F=this.sessionKey,A=_,j=$,H=!1;F&&(A=p.cryptoService.encryptContent(_,F),j={encrypted:p.cryptoService.encryptMetadata($,F)},H=!0);let G=this.nextPromptGen++,K={promptId:f,gen:G};n.pendingPromptId=f,n.pendingSubmitMap=k.submitMap,n.promptGenerationToken=K;let C=K,N=this.activeSessions.get(e)?.promptGenerationToken;if(!N||N.gen!==C.gen||N.promptId!==C.promptId){i.warn("Q[next] emit aborted: token replaced before await dispatch",{sessionId:e,tokenAtAwait:C,currentToken:N});return}let oe=f.length>0?f:`__prompt_gen_${K.gen}`;n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightPromptIds.add(oe);try{try{await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.INTERACTIVE_PROMPT,source:p.EventSource.DESKTOP,content:A,metadata:j,promptId:f,timestamp:(0,p.prepareEventTimestamp)({orderingKey:e}),isEncrypted:H?!0:void 0}),i.info("Q[next] emit succeeded",{sessionId:e,promptId:f,remaining:n.pendingQuestionsQueue?.length??0})}catch(M){let L=this.activeSessions.get(e),q=L?.promptGenerationToken;q&&q.gen===C.gen&&q.promptId===C.promptId?(this.promoteFingerprintAndClearPromptState(L),i.warn("Q[next] emit failed; promoted fingerprint + cleared prompt state. User must answer remaining questions on desktop terminal.",{sessionId:e,promptId:f,error:M instanceof Error?M.message:String(M)})):i.warn("Q[next] emit failed but a NEW prompt replaced our token during await; not clearing state (would wipe new prompt). Q[next..QN] of the original AskUserQuestion are lost; new prompt continues normally.",{sessionId:e,tokenAtAwait:C,currentToken:q,error:M instanceof Error?M.message:String(M)})}}finally{n.inFlightPromptIds.delete(oe)}}else try{await this.sendPromptError(e,"Failed to select option")}catch(y){i.warn("[walker] sendPromptError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(y)})}finally{await this.markEventExecutedIdempotent(n,t)}}else if(v.action==="option_with_followup"){if(n.pendingQuestionsQueue!==void 0){let y=this.getWalkerPosition(n),P=n.pendingCurrentQuestion?.options?.length??0,f=v.option?parseInt(v.option,10):NaN,U=Number.isFinite(f)&&f>P,k=v.followUpText??"";if(i.info("AUQ walker \u2014 option_with_followup dispatch",{sessionId:e,option:v.option,optionNum:f,explicitCount:P,isSynthDigit:U,walkerPosition:y,followUpTextLen:k.length}),await this.markEventExecutedIdempotent(n,t),l())return;if(U){if(y==="on_synth"){let C=await this.promptResponder.answerInteractivePrompt(e,"2",{pressEnter:!1});if(l())return;if(!C){try{await this.emitUserChoice(e,"AskUserQuestion cancel keypress failed (tmux unavailable); your reply was not sent")}catch(b){i.warn("emitUserChoice on synth-digit on-SYNTH Cancel failed",{sessionId:e,error:b instanceof Error?b.message:String(b)})}return}if(await new Promise(b=>setTimeout(b,1500)),l())return;if(k){let b=await this.promptResponder.answerInteractivePrompt(e,k,{pressEnter:!0});if(l())return;b||i.warn("on-SYNTH Cancel followup text-send failed",{sessionId:e})}try{await this.emitUserChoice(e,"\u2192 Cancel \u2014 AskUserQuestion cancelled, sending your reply as a new prompt")}catch(b){i.warn("emitUserChoice on synth-digit on-SYNTH success failed",{sessionId:e,error:b instanceof Error?b.message:String(b)})}if(l())return;this.promoteFingerprintAndClearPromptState(n);return}await this.handleMobileReplyAsDismissAndPrompt(e,n,k,l);return}let _=R?.[v.option]||v.option,$=f-1,F=n.pendingCurrentQuestion?.options?.[$],A=typeof F=="string"?F:F&&typeof F=="object"?F.label:`option ${v.option}`;if(y==="q1"){let C=await this.promptResponder.answerInteractivePrompt(e,_,{pressEnter:!1});if(l())return;if(!C){try{await this.sendPromptError(e,"Failed to select option. Your reply (including the follow-up text) was not sent. Please retry.")}catch(b){i.warn("sendPromptError on q1 option_with_followup failed",{sessionId:e,error:b instanceof Error?b.message:String(b)})}return}if(await new Promise(b=>setTimeout(b,1500)),l())return;if(k){let b=await this.promptResponder.answerInteractivePrompt(e,k,{pressEnter:!0});if(l())return;b||i.warn("q1 option_with_followup followup text-send failed",{sessionId:e})}try{await this.emitUserChoice(e,k?`\u2192 ${A} + sending your reply as a new prompt`:`\u2192 ${A}`)}catch(b){i.warn("emitUserChoice on q1 option_with_followup success failed",{sessionId:e,error:b instanceof Error?b.message:String(b)})}if(l())return;this.promoteFingerprintAndClearPromptState(n);return}if(y==="nq_mid"){let C=await this.promptResponder.answerInteractivePrompt(e,_,{pressEnter:!1});if(l())return;if(!C){try{await this.sendPromptError(e,"Failed to commit Q[i]. Your reply was not sent. Please retry.")}catch(N){i.warn("sendPromptError on nq_mid option_with_followup failed",{sessionId:e,error:N instanceof Error?N.message:String(N)})}return}if(await new Promise(N=>setTimeout(N,200)),l())return;let b=n.pendingQuestionsQueue?.shift();b?n.pendingCurrentQuestion=b:i.warn("nq_mid option_with_followup: queue.shift returned undefined unexpectedly",{sessionId:e}),await this.handleMobileReplyAsDismissAndPrompt(e,n,k,l);return}if(y==="nq_last"){await this.handleOptionWithFollowupOnLastQ(e,n,_,A,k,l);return}let j=await this.promptResponder.answerInteractivePrompt(e,_,{pressEnter:!1});if(l())return;if(!j){try{await this.sendPromptError(e,"Failed to send SYNTH keypress. Your reply was not sent. Please retry.")}catch(C){i.warn("sendPromptError on on-SYNTH option_with_followup failed",{sessionId:e,error:C instanceof Error?C.message:String(C)})}return}if(await new Promise(C=>setTimeout(C,1500)),l())return;if(k){let C=await this.promptResponder.answerInteractivePrompt(e,k,{pressEnter:!0});if(l())return;C||i.warn("on-SYNTH option_with_followup text-send failed",{sessionId:e})}let H=n.pendingCurrentQuestion?._isSubmit===!0,G=A.toLowerCase(),K;H&&G==="cancel"?K=k?"\u2192 Cancel \u2014 AskUserQuestion cancelled, sending your reply as a new prompt":"\u2192 Cancel \u2014 AskUserQuestion cancelled, no answers submitted":H&&G.startsWith("submit")?K=k?"\u2192 Submit answers \u2014 AskUserQuestion completed, sending your reply as a new prompt":"\u2192 Submit answers \u2014 AskUserQuestion completed":K=k?`\u2192 ${A} + sending your reply as a new prompt`:`\u2192 ${A}`;try{await this.emitUserChoice(e,K)}catch(C){i.warn("emitUserChoice on on-SYNTH option_with_followup success failed",{sessionId:e,error:C instanceof Error?C.message:String(C)})}if(l())return;this.promoteFingerprintAndClearPromptState(n);return}let I=R?.[v.option]||v.option;i.info("User selected option with follow-up",{option:v.option,terminalInput:I,followUpText:v.followUpText});let E=await this.promptResponder.answerInteractivePrompt(e,I);if(l()){await this.markEventExecutedIdempotent(n,t);return}if(E){if(await this.markEventExecutedIdempotent(n,t),l())return;if(!m){i.warn("emitAnswerAck called without promptId \u2014 clearing state + skipping ack",{sessionId:e,source:"option_with_followup",eventId:t.eventId}),this.promoteFingerprintAndClearPromptState(n);return}try{await this.emitAnswerAck(e,`Selected option ${v.option}`,{promptId:m,questionIndex:0,isTerminal:!0})}catch(y){i.warn("[walker] emitAnswerAck (option_with_followup) failed \u2014 continuing to clearPromptState + executeMobilePrompt",{sessionId:e,promptId:m,error:y instanceof Error?y.message:String(y)})}if(l())return;if(this.promoteFingerprintAndClearPromptState(n),v.followUpText){await new Promise(P=>setTimeout(P,1e3));let y={...t,content:v.followUpText};await this.executeMobilePrompt(e,y)}}else try{await this.sendPromptError(e,"Failed to select option. Your reply (including the follow-up text) was not sent. Please retry.")}catch(y){i.warn("[walker] sendPromptError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(y)})}finally{await this.markEventExecutedIdempotent(n,t)}}else{if(n.pendingQuestionsQueue!==void 0){let E=this.getWalkerPosition(n);if(i.info("AUQ walker \u2014 send_as_response dispatch",{sessionId:e,walkerPosition:E,contentPreview:d.slice(0,80)}),await this.markEventExecutedIdempotent(n,t),l())return;if(E==="on_synth"){let y=await this.promptResponder.answerInteractivePrompt(e,"2",{pressEnter:!1});if(l())return;if(!y){try{await this.emitUserChoice(e,"AskUserQuestion cancel keypress failed (tmux unavailable); your reply was not sent")}catch(f){i.warn("emitUserChoice on send_as_response on-SYNTH Cancel failed",{sessionId:e,error:f instanceof Error?f.message:String(f)})}return}if(await new Promise(f=>setTimeout(f,1500)),l())return;let P=await this.promptResponder.answerInteractivePrompt(e,d,{pressEnter:!0});if(l())return;P||i.warn("send_as_response on-SYNTH text-send failed",{sessionId:e});try{await this.emitUserChoice(e,"\u2192 Cancel \u2014 AskUserQuestion cancelled, sending your reply as a new prompt")}catch(f){i.warn("emitUserChoice on send_as_response on-SYNTH success failed",{sessionId:e,error:f instanceof Error?f.message:String(f)})}if(l())return;this.promoteFingerprintAndClearPromptState(n);return}await this.handleMobileReplyAsDismissAndPrompt(e,n,d,l);return}i.info("Sending as free-form response to interactive prompt",{response:d});let I=await this.promptResponder.answerInteractivePrompt(e,d);if(l()){await this.markEventExecutedIdempotent(n,t);return}if(I){if(await this.markEventExecutedIdempotent(n,t),l())return;if(!m){i.warn("emitAnswerAck called without promptId \u2014 clearing state + skipping ack",{sessionId:e,source:"send_as_response",eventId:t.eventId}),this.promoteFingerprintAndClearPromptState(n);return}try{await this.emitAnswerAck(e,"Response sent to interactive prompt",{promptId:m,questionIndex:0,isTerminal:!0})}catch(E){i.warn("[walker] emitAnswerAck (send_as_response) failed \u2014 continuing to clearPromptState",{sessionId:e,promptId:m,error:E instanceof Error?E.message:String(E)})}if(l())return;this.promoteFingerprintAndClearPromptState(n)}else try{await this.sendPromptError(e,"Failed to send response")}catch(E){i.warn("[walker] sendPromptError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(E)})}finally{await this.markEventExecutedIdempotent(n,t)}}}finally{u&&n.inFlightPromptIds&&n.inFlightPromptIds.delete(u),g&&n.inFlightEventIds&&n.inFlightEventIds.delete(g)}}async sendInteractivePromptAsync(e,t,r){let n=this.activeSessions.get(e),s=n?.promptGenerationToken?{...n.promptGenerationToken}:void 0,o=(0,p.prepareEventTimestamp)({orderingKey:e});await new Promise(l=>setTimeout(l,500));let a=process.env.CODEVIBE_TMUX_SESSION,c={...t.metadata||{}},h=t.metadata?.tool_name,u=t.metadata?.tool_input,g=h==="AskUserQuestion"&&Array.isArray(u?.questions)?u.questions:[];if(g.length>0&&Array.isArray(g[0]?.options)&&g[0].options.length>0){let l=g[0],T=[];if(a)try{let{exec:f}=await import("child_process"),U=_=>new Promise(($,F)=>{f(_,{timeout:5e3},(A,j)=>{A?F(A):$({stdout:j||""})})}),{stdout:k}=await U(`tmux capture-pane -p -e -S -30 -t '${a}'`);T=this.parseAskUserQuestionSynthTail(k),i.info("AskUserQuestion synth-tail parsed from tmux",{tailCount:T.length,tail:T.map(_=>_.label)})}catch(f){i.warn("Failed to capture tmux for AskUserQuestion synth-tail; emitting without synth tail",{error:f instanceof Error?f.message:String(f)})}else i.info("No tmux session \u2014 AskUserQuestion synth tail will be empty");let I=this.activeSessions.get(e);if(I){let f=I.promptGenerationToken;s&&f?.gen===s.gen?I.pendingSynthTail=T:i.warn("AskUserQuestion synth-tail: stale async \u2014 token gen mismatch, skipping pendingSynthTail write",{tokenAtEmit:s,currentToken:f,sessionId:e})}let E=this.buildQuestionWireData(l,T);c.options=JSON.parse(JSON.stringify(E.options)),c.submitMap=JSON.parse(JSON.stringify(E.submitMap)),c.instructions=E.instructions,c.tool_name="AskUserQuestion",c.tool_input={questions:[l]},r=l.question;let y=typeof t.prompt_id=="string"&&t.prompt_id.length>0,P=Me(g);if(y){let f=this.activeSessions.get(e);if(f){let U=f.promptGenerationToken;s&&U?.gen===s.gen?(f.pendingQuestionsQueue=P.remainingQueue,f.pendingCurrentQuestion=l,f.hasReviewScreen=P.hasReviewScreen):i.warn("AskUserQuestion: stale async \u2014 token gen mismatch, skipping walker-state write",{tokenAtEmit:s,currentToken:U,sessionId:e})}}else i.warn("AskUserQuestion: empty prompt_id, degrading to single-Q legacy emit",{questionCount:g.length});i.info("AskUserQuestion: emitting Q1 only (Q2..QN queued)",{questionCount:P.questionCount,hasReviewScreen:P.hasReviewScreen,queuedRemaining:y?P.remainingQueue.length:0,optionCountFirst:E.options.length,questionPreview:l.question.slice(0,80)})}else if(a)try{let{exec:l}=await import("child_process"),T=P=>new Promise((f,U)=>{l(P,{timeout:5e3},(k,_)=>{k?U(k):f({stdout:_||""})})}),{stdout:I}=await T(`tmux capture-pane -p -e -S -30 -t '${a}'`),E=I.split(`
6
+ `);i.info("tmux capture result",{tmuxSession:a,totalLines:E.length,lastLines:E.slice(-15).map(P=>P.replace(/\x1B[^m]*m/g,"").trim()).filter(Boolean)});let y=(0,p.parseInteractivePrompt)(I);if(y&&y.options.length>0)c.options=y.options,c.submitMap=y.submitMap,c.instructions=this.buildPromptInstructions(y),i.info("Parsed dynamic options from tmux",{optionCount:y.options.length,kind:y.kind,options:y.options});else{this.suppressPromptOptionsUnavailable(e,s,"tmux parse returned no options");return}}catch(l){this.suppressPromptOptionsUnavailable(e,s,`tmux capture failed: ${l instanceof Error?l.message:String(l)}`);return}else{this.suppressPromptOptionsUnavailable(e,s,"no tmux session");return}let d=this.activeSessions.get(e);if(d&&c.submitMap){let l=d.promptGenerationToken;s&&l?.gen===s.gen?d.pendingSubmitMap=c.submitMap:i.warn("Interactive prompt async: stale async \u2014 token gen mismatch, skipping pendingSubmitMap write",{tokenAtEmit:s,currentToken:l,sessionId:e})}let m=r,x=c,R=!1;this.sessionKey&&(m=p.cryptoService.encryptContent(r,this.sessionKey),x={encrypted:p.cryptoService.encryptMetadata(x,this.sessionKey)},R=!0);let v=this.activeSessions.get(e)?.promptGenerationToken;if(s&&v?.gen!==s.gen){i.warn("Interactive prompt emit: stale token \u2014 newer INTERACTIVE_PROMPT replaced ours; skipping AppSync emit",{sessionId:e,tokenAtEmit:s,currentToken:v});return}await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.INTERACTIVE_PROMPT,source:t.source,content:m,metadata:x,promptId:t.prompt_id,timestamp:o,isEncrypted:R?!0:void 0}),i.info("Interactive prompt sent to AppSync with dynamic options",{sessionId:e})}buildQuestionWireData(e,t=[]){let r=(e.options||[]).map((c,h)=>{let u=typeof c=="string",g=u?c:c.label||"",d=u?"":c.description||"",m=u?"":c.preview||"",x={number:String(h+1),text:g};return d&&(x.description=d),m&&(x.preview=m),x});if(!e._isSubmit)for(let c of t)r.push({number:String(r.length+1),text:c.label});let n=Object.fromEntries(r.map(c=>[c.number,c.number])),s=(e.options||[]).length,o=t.findIndex(c=>c.label==="Type something"),a;if(e._isSubmit)a="Reply with 1 to submit your answers or 2 to cancel.";else if(e.multiSelect)a=`Reply with comma-separated numbers (e.g., 1,3) for "${e.header||e.question}"`;else if(o>=0){let c=String(s+o+1);a=`Reply with the number of your choice. For option ${c} (Type something), reply "${c}, your answer".`}else a="Reply with the number of your choice.";return{options:r,submitMap:n,instructions:a}}parseAskUserQuestionSynthTail(e){let t=(0,p.normalizeSnapshot)(e);if(!t)return[];let r=t.split(`
7
+ `).slice(-14),n=[],s=new Set,o=/^\s*(?:[›❯▸▶➜➤]\s*)?(?:\d+\.\s+)?(Type something|Chat about this)\.?\s*$/i;for(let a of r){let c=a.match(o);if(!c)continue;let u=c[1].toLowerCase()==="type something"?"Type something":"Chat about this";s.has(u)||(n.push({label:u}),s.add(u))}return n}suppressPromptOptionsUnavailable(e,t,r){i.warn("Interactive prompt: real options unavailable \u2014 suppressing mobile prompt (no fabricated options)",{sessionId:e,reason:r});let n=this.activeSessions.get(e);n&&t&&n.promptGenerationToken?.gen===t.gen&&this.clearPromptState(n)}buildPromptInstructions(e){return`Reply with ${e.options.map(r=>r.number).join(", ")}. Append a message to provide alternative instructions.`}parseInteractivePromptInput(e,t=3){return Pe(e,t)}async markEventExecuted(e){try{await this.appSyncClient.updateEventStatus({eventId:e.eventId,sessionId:e.sessionId,timestamp:e.timestamp,deliveryStatus:p.DeliveryStatus.EXECUTED}),i.info("Event marked as EXECUTED",{eventId:e.eventId})}catch(t){i.warn("Failed to mark event as EXECUTED",{eventId:e.eventId,error:t})}}async sendPromptError(e,t){let r={error:!0},n=t,s=r,o=!1;this.sessionKey&&(n=p.cryptoService.encryptContent(t,this.sessionKey),s={encrypted:p.cryptoService.encryptMetadata(r,this.sessionKey)},o=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:n,metadata:s,timestamp:(0,p.prepareEventTimestamp)({orderingKey:e}),isEncrypted:o?!0:void 0})}async emitUserChoice(e,t){let r=t,n={source:"codevibe_v2_user_choice"},s=!1;this.sessionKey&&(r=p.cryptoService.encryptContent(t,this.sessionKey),n={encrypted:p.cryptoService.encryptMetadata({source:"codevibe_v2_user_choice"},this.sessionKey)},s=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.ASSISTANT_RESPONSE,source:p.EventSource.DESKTOP,content:r,metadata:n,timestamp:(0,p.prepareEventTimestamp)({orderingKey:e}),isEncrypted:s?!0:void 0})}async emitAnswerAck(e,t,r){let n={promptAnswered:!0,...r},s=t,o=n,a=!1;this.sessionKey&&(s=p.cryptoService.encryptContent(t,this.sessionKey),o={encrypted:p.cryptoService.encryptMetadata(n,this.sessionKey)},a=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:s,metadata:o,timestamp:(0,p.prepareEventTimestamp)({orderingKey:e}),isEncrypted:a?!0:void 0})}promoteFingerprintAndClearPromptState(e){e.activeAskUserQuestionFingerprint&&(e.completedAskUserQuestionFingerprints||(e.completedAskUserQuestionFingerprints=new Set),e.completedAskUserQuestionFingerprints.add(e.activeAskUserQuestionFingerprint)),this.clearPromptState(e)}clearPromptState(e){e.waitingForPromptResponse=!1,e.pendingPromptId=void 0,e.pendingSubmitMap=void 0,e.pendingQuestionsQueue=void 0,e.pendingCurrentQuestion=void 0,e.pendingSynthTail=void 0,e.hasReviewScreen=void 0,e.activeAskUserQuestionFingerprint=void 0,e.promptGenerationToken=void 0}async detectWalkerActive(e){let t=process.env.CODEVIBE_TMUX_SESSION;if(!t)return i.debug("detectWalkerActive: no CODEVIBE_TMUX_SESSION env \u2014 conservatively assuming walker active",{sessionId:e}),!0;try{let{exec:r}=await import("child_process"),n=l=>new Promise((T,I)=>{r(l,{timeout:3e3},(E,y)=>{E?I(E):T({stdout:y||""})})}),{stdout:s}=await n(`tmux capture-pane -p -e -S -30 -t '${t}'`),o=/\x1B(?:\[[?!0-9;]*[A-Za-z]|\][^\x07\x1B]*(?:\x07|\x1B\\)|[()][A-B012])/g,a=s.split(`
8
+ `).map(l=>l.replace(o,"").trimEnd()),c=a.length-1;for(;c>=0&&a[c].trim()==="";)c-=1;if(c<0)return i.warn("detectWalkerActive: pane empty after ANSI strip \u2014 conservatively assuming walker active",{sessionId:e}),!0;let h=/(?:Esc to cancel|Enter to select|↑\/↓ to navigate|Tab to switch|shift\+tab|Chat about this|Type something|Notes: press|\(esc\)|\(shift\+tab\))/i,u=/^\s*(?:[›❯▸▶➜➤]\s*)?\d+\.\s+/,g=-1;for(let l=c;l>=0;l-=1)if(u.test(a[l])){g=l;break}let d=g>=0,m=-1;for(let l=c;l>=0;l-=1)if(h.test(a[l])){m=l;break}let x=m<0?1/0:c-m,R=m>=0&&x<=3,Q=m>=0&&g>=0&&m>=g&&m-g<=5,v=d&&R&&Q;return i.info("detectWalkerActive",{sessionId:e,walkerActive:v,hasParserBlock:d,chromeNearBottom:R,chromeFollowsBlock:Q,chromeDistanceFromBottom:Number.isFinite(x)?x:-1,parserBlockEndsAt:g,lastNonBlank:c,lastChromeLineAt:m,lastLineSample:a[c]?.slice(0,80)??""}),v}catch(r){return i.warn("detectWalkerActive: tmux capture/parse failed \u2014 conservatively assuming walker active",{sessionId:e,error:r instanceof Error?r.message:String(r)}),!0}}getWalkerPosition(e){return e.pendingCurrentQuestion?._isSubmit?"on_synth":e.hasReviewScreen?(e.pendingQuestionsQueue?.length??0)>=2?"nq_mid":"nq_last":"q1"}async emitDegradedAUQNotification(e,t){try{let r=t,n={source:"codevibe_auq_degraded"},s=!1;this.sessionKey&&(r=p.cryptoService.encryptContent(t,this.sessionKey),n={encrypted:p.cryptoService.encryptMetadata({source:"codevibe_auq_degraded"},this.sessionKey)},s=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.ASSISTANT_RESPONSE,source:p.EventSource.DESKTOP,content:r,metadata:n,isEncrypted:s?!0:void 0}),i.info("Emitted degraded-AUQ notification",{sessionId:e,textPreview:t.slice(0,60)})}catch(r){i.warn("emitDegradedAUQNotification failed",{sessionId:e,error:r instanceof Error?r.message:String(r)})}}async handleMobileReplyAsDismissAndPrompt(e,t,r,n){let s=await this.promptResponder.sendKey(e,"Escape");if(n())return!1;if(!s){try{await this.emitUserChoice(e,"AskUserQuestion dismissal failed (tmux unavailable)")}catch(a){i.warn("emitUserChoice on dismiss-ok1-failed failed",{sessionId:e,error:a instanceof Error?a.message:String(a)})}return!1}if(await new Promise(a=>setTimeout(a,200)),n())return!1;let o=await this.promptResponder.answerInteractivePrompt(e,r,{pressEnter:!0});if(n())return!1;if(!o){try{await this.emitUserChoice(e,"AskUserQuestion dismissed but text-send failed; please retry")}catch(a){i.warn("emitUserChoice on dismiss-ok2-failed failed",{sessionId:e,error:a instanceof Error?a.message:String(a)})}return n()||this.promoteFingerprintAndClearPromptState(t),!1}try{await this.emitUserChoice(e,"\u2192 AskUserQuestion dismissed, sending your reply as a new prompt")}catch(a){i.warn("emitUserChoice on dismiss-success failed \u2014 continuing to promote+clear",{sessionId:e,error:a instanceof Error?a.message:String(a)})}return n()?(i.warn("handleMobileReplyAsDismissAndPrompt: aborted post-emitUserChoice (token rotated); leaving session state for new prompt",{sessionId:e}),!1):(this.promoteFingerprintAndClearPromptState(t),!0)}async handleOptionWithFollowupOnLastQ(e,t,r,n,s,o){let a=await this.promptResponder.answerInteractivePrompt(e,r,{pressEnter:!1});if(o())return!1;if(!a){try{await this.emitUserChoice(e,"Failed to commit Q[N] (tmux unavailable); your reply was not sent")}catch(u){i.warn("emitUserChoice on lastQ-ok1-failed failed",{sessionId:e,error:u instanceof Error?u.message:String(u)})}return!1}if(await new Promise(u=>setTimeout(u,200)),o())return!1;let c=await this.promptResponder.answerInteractivePrompt(e,"1",{pressEnter:!1});if(o())return!1;if(!c){try{await this.emitUserChoice(e,"Q[N] committed but Submit failed (tmux unavailable); please retry on desktop")}catch(u){i.warn("emitUserChoice on lastQ-ok2-failed failed",{sessionId:e,error:u instanceof Error?u.message:String(u)})}return o()||this.promoteFingerprintAndClearPromptState(t),!1}if(await new Promise(u=>setTimeout(u,1500)),o())return!1;let h=await this.promptResponder.answerInteractivePrompt(e,s,{pressEnter:!0});if(o())return!1;h||i.warn("handleOptionWithFollowupOnLastQ: followup text send failed",{sessionId:e});try{await this.emitUserChoice(e,`\u2192 ${n} + sending your reply as a new prompt`)}catch(u){i.warn("emitUserChoice on lastQ-success failed \u2014 continuing to promote+clear",{sessionId:e,error:u instanceof Error?u.message:String(u)})}return o()?(i.warn("handleOptionWithFollowupOnLastQ: aborted post-emitUserChoice (token rotated); leaving session state for new prompt",{sessionId:e}),!1):(this.promoteFingerprintAndClearPromptState(t),!0)}computeAskUserQuestionFingerprint(e){if(!(!e||typeof e!="object"))try{let t=this.stringifyCanonical(e);return(0,W.createHash)("sha256").update(t).digest("hex")}catch(t){i.warn("Failed to fingerprint AskUserQuestion questions",{error:t instanceof Error?t.message:String(t)});return}}stringifyCanonical(e){return e===null||typeof e!="object"?JSON.stringify(e):Array.isArray(e)?"["+e.map(r=>this.stringifyCanonical(r)).join(",")+"]":"{"+Object.keys(e).sort().map(r=>JSON.stringify(r)+":"+this.stringifyCanonical(e[r])).join(",")+"}"}async markEventExecutedIdempotent(e,t){e.processedEventIds||(e.processedEventIds=new Set),e.processedEventIds.add(t.eventId);try{await this.markEventExecuted(t)}catch(r){i.warn("[walker] markEventExecuted threw \u2014 relying on processedEventIds set for dedup",{sessionId:t.sessionId,eventId:t.eventId,error:String(r)})}}isSessionLimitExceeded(e){return this.getErrorMessage(e).includes("SESSION_LIMIT_EXCEEDED")}isUsageLimitExceeded(e){let t=this.getErrorMessage(e);return t.includes("MESSAGE_LIMIT_EXCEEDED")||t.includes("IMAGE_LIMIT_EXCEEDED")}getErrorMessage(e){if(e instanceof Error)return e.message;if(typeof e=="object"&&e!==null){let t=e;if(t.errors&&Array.isArray(t.errors))return t.errors.map(r=>r.message||"").join(" ");if(typeof t.message=="string")return t.message}return String(e)}displaySubscriptionLimitError(e,t){let r=this.getErrorMessage(e),n="",s=r.match(/for your (\w+) plan/i);s&&(n=` (${s[1]} tier)`);let o="",a=r.match(/of (\d+)/);switch(a&&(o=` [Limit: ${a[1]}]`),console.log(`
9
9
  `+"=".repeat(60)),console.log("\u26A0\uFE0F SUBSCRIPTION LIMIT REACHED"),console.log("=".repeat(60)),t){case"session":console.log(`You have reached the maximum number of active sessions${n}.`),console.log(`${o}`),console.log(`
10
10
  To continue, please:`),console.log(" \u2022 Close an existing Claude Code session, or"),console.log(" \u2022 Upgrade your subscription in the CodeVibe iOS app");break;case"message":console.log(`You have reached your monthly message limit${n}.`),console.log(`${o}`),console.log(`
11
11
  To continue, please:`),console.log(" \u2022 Wait until your usage resets next month, or"),console.log(" \u2022 Upgrade your subscription in the CodeVibe iOS app");break;case"image":console.log(`You have reached your monthly image attachment limit${n}.`),console.log(`${o}`),console.log(`
12
12
  To continue, please:`),console.log(" \u2022 Wait until your usage resets next month, or"),console.log(" \u2022 Upgrade your subscription in the CodeVibe iOS app");break}console.log(`
13
13
  Note: You can still use Claude Code normally from your desktop.`),console.log("This limit only affects syncing with the mobile app."),console.log("=".repeat(60)+`
14
- `),i.error("Subscription limit exceeded",{limitType:t,errorMessage:r})}async downloadAttachment(e,t,r){try{let n=e.isEncrypted??r??!1;i.info("Downloading attachment - START",{id:e.id,type:e.type,filename:e.filename,s3Key:e.s3Key,attachmentIsEncrypted:e.isEncrypted,eventIsEncrypted:r,shouldDecrypt:n,hasSessionKey:!!this.sessionKey});let{downloadUrl:s}=await this.appSyncClient.getAttachmentDownloadUrl(e.s3Key),o=await fetch(s);if(!o.ok)throw new Error(`Failed to download attachment: ${o.status} ${o.statusText}`);let a=Buffer.from(await o.arrayBuffer());if(i.info("Attachment downloaded",{id:e.id,downloadedSize:a.length,first20Bytes:a.slice(0,20).toString("hex")}),i.info("Checking decryption conditions",{id:e.id,shouldDecrypt:n,hasSessionKey:!!this.sessionKey,willDecrypt:!!(n&&this.sessionKey)}),n&&this.sessionKey)try{i.info("Decrypting attachment",{id:e.id,encryptedSize:a.length}),a=p.cryptoService.decryptData(a,this.sessionKey),i.info("Attachment decrypted successfully",{id:e.id,decryptedSize:a.length,first20Bytes:a.slice(0,20).toString("hex")})}catch(m){throw i.error("Failed to decrypt attachment:",{id:e.id,error:m}),new Error("Failed to decrypt attachment")}else n&&!this.sessionKey?i.warn("Cannot decrypt attachment - no session key available",{id:e.id}):i.info("Skipping decryption - attachment not encrypted or no session key",{id:e.id,shouldDecrypt:n,hasSessionKey:!!this.sessionKey});let c=V.join(ee.tmpdir(),"codevibe-claude",t);D.existsSync(c)||D.mkdirSync(c,{recursive:!0});let f="",u=e.filename;if(n&&e.filename&&this.sessionKey)try{u=p.cryptoService.decryptContent(e.filename,this.sessionKey)}catch{u=e.filename}if(u){let m=V.extname(u);m&&(f=m)}f||(f={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[e.type]||".bin");let g=`attachment-${e.id}${f}`,d=V.join(c,g);return D.writeFileSync(d,a),i.info("Attachment saved to temp file",{id:e.id,filePath:d,size:a.length,wasDecrypted:n&&!!this.sessionKey}),d}catch(n){return i.error("Failed to download attachment:",{id:e.id,error:n}),null}}async executeMobilePrompt(e,t){let r=t.content||"",n=t.attachments||[];i.info("Executing mobile prompt via tmux",{sessionId:e,promptLength:r.length,attachmentCount:n.length});let s=[];if(n.length>0){i.info("Downloading attachments for prompt",{count:n.length});for(let o of n){let a=await this.downloadAttachment(o,e,t.isEncrypted);a&&s.push(a)}if(s.length>0){let o=s.map(a=>`[Attached file: ${a}]`).join(`
14
+ `),i.error("Subscription limit exceeded",{limitType:t,errorMessage:r})}async downloadAttachment(e,t,r){try{let n=e.isEncrypted??r??!1;i.info("Downloading attachment - START",{id:e.id,type:e.type,filename:e.filename,s3Key:e.s3Key,attachmentIsEncrypted:e.isEncrypted,eventIsEncrypted:r,shouldDecrypt:n,hasSessionKey:!!this.sessionKey});let{downloadUrl:s}=await this.appSyncClient.getAttachmentDownloadUrl(e.s3Key),o=await fetch(s);if(!o.ok)throw new Error(`Failed to download attachment: ${o.status} ${o.statusText}`);let a=Buffer.from(await o.arrayBuffer());if(i.info("Attachment downloaded",{id:e.id,downloadedSize:a.length,first20Bytes:a.slice(0,20).toString("hex")}),i.info("Checking decryption conditions",{id:e.id,shouldDecrypt:n,hasSessionKey:!!this.sessionKey,willDecrypt:!!(n&&this.sessionKey)}),n&&this.sessionKey)try{i.info("Decrypting attachment",{id:e.id,encryptedSize:a.length}),a=p.cryptoService.decryptData(a,this.sessionKey),i.info("Attachment decrypted successfully",{id:e.id,decryptedSize:a.length,first20Bytes:a.slice(0,20).toString("hex")})}catch(m){throw i.error("Failed to decrypt attachment:",{id:e.id,error:m}),new Error("Failed to decrypt attachment")}else n&&!this.sessionKey?i.warn("Cannot decrypt attachment - no session key available",{id:e.id}):i.info("Skipping decryption - attachment not encrypted or no session key",{id:e.id,shouldDecrypt:n,hasSessionKey:!!this.sessionKey});let c=V.join(ee.tmpdir(),"codevibe-claude",t);D.existsSync(c)||D.mkdirSync(c,{recursive:!0});let h="",u=e.filename;if(n&&e.filename&&this.sessionKey)try{u=p.cryptoService.decryptContent(e.filename,this.sessionKey)}catch{u=e.filename}if(u){let m=V.extname(u);m&&(h=m)}h||(h={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[e.type]||".bin");let g=`attachment-${e.id}${h}`,d=V.join(c,g);return D.writeFileSync(d,a),i.info("Attachment saved to temp file",{id:e.id,filePath:d,size:a.length,wasDecrypted:n&&!!this.sessionKey}),d}catch(n){return i.error("Failed to download attachment:",{id:e.id,error:n}),null}}async executeMobilePrompt(e,t){let r=t.content||"",n=t.attachments||[];i.info("Executing mobile prompt via tmux",{sessionId:e,promptLength:r.length,attachmentCount:n.length});let s=[];if(n.length>0){i.info("Downloading attachments for prompt",{count:n.length});for(let o of n){let a=await this.downloadAttachment(o,e,t.isEncrypted);a&&s.push(a)}if(s.length>0){let o=s.map(a=>`[Attached file: ${a}]`).join(`
15
15
  `);r?r=`${o}
16
16
 
17
17
  ${r}`:r=`${o}