@quantiya/codevibe-claude-plugin 1.0.32 → 1.0.33

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevibe-claude",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
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 Q=Object.create;var P=Object.defineProperty;var J=Object.getOwnPropertyDescriptor;var Z=Object.getOwnPropertyNames;var ee=Object.getPrototypeOf,te=Object.prototype.hasOwnProperty;var se=(l,e)=>{for(var s in e)P(l,s,{get:e[s],enumerable:!0})},F=(l,e,s,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Z(e))!te.call(l,i)&&i!==s&&P(l,i,{get:()=>e[i],enumerable:!(t=J(e,i))||t.enumerable});return l};var y=(l,e,s)=>(s=l!=null?Q(ee(l)):{},F(e||!l||!l.__esModule?P(s,"default",{value:l,enumerable:!0}):s,l)),ne=l=>F(P({},"__esModule",{value:!0}),l);var ce={};se(ce,{parseInteractivePromptInput:()=>X});module.exports=ne(ce);var v=y(require("fs")),E=y(require("path")),T=y(require("os")),H=require("child_process"),q=require("util"),W=require("child_process");var O=y(require("os")),D=y(require("path")),N=require("@quantiya/codevibe-core"),n=(0,N.createLogger)({name:"codevibe-claude",logFile:D.default.join(O.default.tmpdir(),"codevibe-claude-mcp.log"),level:"info"});var a=require("@quantiya/codevibe-core");var R=y(require("express")),w=y(require("fs")),M=y(require("path")),k=y(require("os")),$=require("@quantiya/codevibe-core");var u=require("@quantiya/codevibe-core");var I=class{constructor(){this.assignedPort=0;this.app=(0,R.default)(),this.setupMiddleware(),this.setupRoutes()}setSessionId(e){this.sessionId=e}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(R.default.json({limit:"1mb"})),this.app.use((e,s,t)=>{n.debug(`${e.method} ${e.path}`,{body:e.body,query:e.query}),t()}),this.app.use((e,s,t,i)=>{n.error("Express error:",e);let r={success:!1,error:e.message||"Internal server error"};t.status(500).json(r)})}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,s){let t={success:!0,data:{status:"healthy",uptime:process.uptime(),version:"0.1.0",timestamp:new Date().toISOString()}};s.json(t)}async handleEvent(e,s){try{let t=e.body;if(!t.session_id){let o={success:!1,error:"Missing required field: session_id"};s.status(400).json(o);return}if(!t.hook_event_name){let o={success:!1,error:"Missing required field: hook_event_name"};s.status(400).json(o);return}let i=this.transformHookToEvent(t);n.info("Received event from hook",{sessionId:t.session_id,hookEvent:t.hook_event_name,type:i.type}),this.eventHandler?await this.eventHandler(i):n.warn("No event handler registered");let r={success:!0,message:"Event processed successfully"};s.json(r)}catch(t){n.error("Error handling event:",t);let i={success:!1,error:t instanceof Error?t.message:"Unknown error"};s.status(500).json(i)}}async handleTestExecute(e,s){try{let{sessionId:t,prompt:i}=e.body;if(!t||!i){let o={success:!1,error:"Missing required fields: sessionId, prompt"};s.status(400).json(o);return}n.info("Test execute request",{sessionId:t,prompt:i});let r={success:!0,message:"Test execution endpoint - not implemented yet",data:{sessionId:t,prompt:i}};s.json(r)}catch(t){n.error("Error in test execute:",t);let i={success:!1,error:t instanceof Error?t.message:"Unknown error"};s.status(500).json(i)}}transformHookToEvent(e){let s,t,i={cwd:e.cwd,hook_event_name:e.hook_event_name,...e.metadata||{}};if(e.type&&e.content!==void 0)s=e.type,t=e.content;else switch(e.hook_event_name){case"SessionStart":s=u.EventType.NOTIFICATION,t="Session started",i.source=e.source;break;case"SessionEnd":s=u.EventType.NOTIFICATION,t=`Session ended: ${e.reason||"unknown"}`,i.reason=e.reason;break;case"UserPromptSubmit":s=u.EventType.USER_PROMPT,t=e.prompt||"";break;case"PostToolUse":s=u.EventType.TOOL_USE,t=JSON.stringify({tool_name:e.tool_name,tool_input:e.tool_input,tool_response:e.tool_response}),i.tool_name=e.tool_name;break;case"Notification":s=u.EventType.NOTIFICATION,t=e.message||"",i.notification_type=e.notification_type;break;default:s=u.EventType.NOTIFICATION,t=`Hook event: ${e.hook_event_name}`}return{session_id:e.session_id,hook_event_name:e.hook_event_name,type:s,source:u.EventSource.DESKTOP,content:t,metadata:i}}onEvent(e){this.eventHandler=e}async start(e){let s=e||this.sessionId;return s&&(this.sessionId=s),new Promise((t,i)=>{try{let r=(0,$.getConfig)(),o=r.server.dynamicPort?0:r.server.port;this.server=this.app.listen(o,r.server.host,()=>{let c=this.server.address();this.assignedPort=c.port,n.info(`HTTP API listening on http://${r.server.host}:${this.assignedPort}`),this.sessionId&&this.writePortFile(this.sessionId,this.assignedPort),t(this.assignedPort)}),this.server.on("error",c=>{n.error("HTTP server error:",c),i(c)})}catch(r){i(r)}})}writePortFile(e,s){let t=M.join(k.tmpdir(),`codevibe-claude-${e}.port`);try{w.writeFileSync(t,s.toString()),n.info(`Port file written: ${t} -> ${s}`)}catch(i){n.error(`Failed to write port file: ${t}`,i)}}removePortFile(){if(this.sessionId){let e=M.join(k.tmpdir(),`codevibe-claude-${this.sessionId}.port`);try{w.existsSync(e)&&(w.unlinkSync(e),n.info(`Port file removed: ${e}`))}catch(s){n.warn(`Failed to remove port file: ${e}`,s)}}}async stop(e){return new Promise((s,t)=>{this.sessionId&&e?.protectedSessionIds?.has(this.sessionId)?n.info("Skipping port file removal \u2014 another daemon still serves this session",{sessionId:this.sessionId}):this.removePortFile(),this.server?this.server.close(i=>{i?(n.error("Error stopping HTTP server:",i),t(i)):(n.info("HTTP API stopped"),s())}):s()})}};var U=require("child_process"),K=require("@quantiya/codevibe-core");var b=class{async executePrompt(e,s){let t=(0,K.getConfig)(),i=t.claude.defaultTimeout;return n.info("Executing prompt from mobile",{sessionId:e,promptLength:s.length,timeout:i}),new Promise(r=>{let o=["--resume",e,"--print","--output-format","stream-json",s];n.debug("Spawning Claude command",{command:t.claude.command,args:o});let c=(0,U.spawn)(t.claude.command,o,{stdio:["pipe","pipe","pipe"],shell:!0}),m="",p="",d=!1,h=setTimeout(()=>{d=!0,n.warn("Command execution timed out",{sessionId:e,timeout:i}),c.kill("SIGTERM")},i);c.stdout?.on("data",g=>{let f=g.toString();m+=f,n.debug("Command stdout",{output:f.slice(0,200)})}),c.stderr?.on("data",g=>{let f=g.toString();p+=f,n.debug("Command stderr",{output:f.slice(0,200)})}),c.on("close",g=>{clearTimeout(h);let f={success:g===0&&!d,output:m,error:p,exitCode:g||void 0,timedOut:d};f.success?n.info("Command executed successfully",{sessionId:e,exitCode:g,outputLength:m.length}):n.error("Command execution failed",{sessionId:e,exitCode:g,timedOut:d,error:p.slice(0,500)}),r(f)}),c.on("error",g=>{clearTimeout(h),n.error("Failed to spawn command",{error:g.message}),r({success:!1,error:g.message,timedOut:!1})})})}detectInteractivePrompt(e){return[/\[Y\/n\]/i,/\[y\/N\]/i,/\(y\/n\)/i,/Continue\?/i,/Proceed\?/i].some(t=>t.test(e))}extractPromptText(e){let s=e.split(`
2
- `);for(let t=s.length-1;t>=0;t--){let i=s[t].trim();if(this.detectInteractivePrompt(i))return i}return null}};var j=require("child_process"),V=require("util");var L=(0,V.promisify)(j.exec),C=class{async answerInteractivePrompt(e,s){n.info("Attempting to answer interactive prompt",{sessionId:e,response:s});try{let t=process.env.CODEVIBE_TMUX_SESSION;return n.info("Checking tmux session environment",{tmuxSession:t||"(not set)",allEnvKeys:Object.keys(process.env).filter(i=>i.includes("CODEVIBE")||i.includes("TMUX"))}),t?(n.info("Using tmux send-keys",{tmuxSession:t}),await this.sendViaTmux(t,s),n.info("Successfully sent response to interactive prompt",{sessionId:e,response:s}),!0):(n.error("No tmux session found - codevibe-claude wrapper is required",{sessionId:e,hint:"Start Claude Code using the codevibe-claude wrapper script"}),!1)}catch(t){return n.error("Failed to answer interactive prompt",{sessionId:e,error:t instanceof Error?t.message:String(t)}),!1}}async sendViaTmux(e,s){let t=s.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");n.info("Sending via tmux",{sessionName:e,inputLength:s.length});try{let i=`tmux send-keys -t "${e}" -l "${t}"`,r=await L(i);n.info("tmux send-keys (text) completed",{stdout:r.stdout||"(empty)",stderr:r.stderr||"(empty)"}),await this.delay(500);let o=`tmux send-keys -t "${e}" Enter`,c=await L(o);n.info("tmux send-keys (Enter) completed",{stdout:c.stdout||"(empty)",stderr:c.stderr||"(empty)"})}catch(i){throw n.error("tmux send-keys failed",{sessionName:e,error:i}),i}}delay(e){return new Promise(s=>setTimeout(s,e))}isPromptResponse(e){let s=e.trim().toLowerCase();return!!(s==="y"||s==="n"||s==="yes"||s==="no"||/^[0-9]+$/.test(s)||/^[a-z]$/.test(s)||["exit","quit","q","continue","skip","abort","retry","cancel"].includes(s))}};var ie=(0,q.promisify)(W.exec),oe="/exit",B="CODEVIBE_TMUX_SESSION";async function re(l,e){let s=async(t,i)=>{try{await ie(t)}catch(r){n.warn("tmux send-keys failed during self-terminate",{sessionName:l,label:i,error:String(r)})}};await s(`tmux send-keys -t "${l}" C-c`,"ctrl-c"),await new Promise(t=>setTimeout(t,200)),await s(`tmux send-keys -t "${l}" -l "${e}"`,"quit-text"),await new Promise(t=>setTimeout(t,500)),await s(`tmux send-keys -t "${l}" Enter`,"enter")}var A=class l{constructor(e){this.activeSessions=new Map;this.assignedPort=0;this.sessionKey=null;this.claudeToBackendSessionId=new Map;this.pendingMobilePrompts=new Map;this.httpApi=new I,this.commandExecutor=new b,this.promptResponder=new C,this.initialSessionId=e}static{this.MOBILE_PROMPT_EXPIRY_MS=3e3}getPort(){return this.assignedPort}generateBackendSessionId(e){return`claude-${e}`}trackMobilePrompt(e,s){this.pendingMobilePrompts.has(e)||this.pendingMobilePrompts.set(e,[]),this.pendingMobilePrompts.get(e).push({prompt:s.trim(),timestamp:Date.now()}),n.debug("Tracking mobile prompt for deduplication",{sessionId:e,promptLength:s.length})}isRecentMobilePrompt(e,s){let t=this.pendingMobilePrompts.get(e);if(!t)return!1;let i=Date.now(),r=s.trim(),o=[],c=!1;for(let m of t)if(!(i-m.timestamp>l.MOBILE_PROMPT_EXPIRY_MS)){if(!c&&m.prompt===r){c=!0,n.debug("Found matching mobile prompt, filtering duplicate",{sessionId:e});continue}o.push(m)}return o.length>0?this.pendingMobilePrompts.set(e,o):this.pendingMobilePrompts.delete(e),c}writePortFile(e){let s=E.join(T.tmpdir(),`codevibe-claude-${e}.port`);try{v.writeFileSync(s,this.assignedPort.toString()),n.info(`Port file written: ${s} -> ${this.assignedPort}`)}catch(t){n.error(`Failed to write port file: ${s}`,t)}}removePortFile(e){let s=E.join(T.tmpdir(),`codevibe-claude-${e}.port`);try{v.existsSync(s)&&(v.unlinkSync(s),n.info(`Port file removed: ${s}`))}catch(t){n.warn(`Failed to remove port file: ${s}`,t)}}hasOtherLiveDaemonForSession(e){try{let s=(0,H.execSync)("ps -eww -o pid= -o args=",{encoding:"utf8",timeout:2e3}),t=process.pid;for(let i of s.split(`
3
- `)){let r=i.trim();if(!r)continue;let o=r.indexOf(" ");if(o<0)continue;let c=parseInt(r.substring(0,o),10);if(isNaN(c)||c===t)continue;let m=r.substring(o+1);if(/node.*codevibe-claude.*server\.js/.test(m)&&m.includes(e))return!0}return!1}catch(s){return n.warn('hasOtherLiveDaemonForSession: ps query failed; falling back to "no other daemon"',{error:String(s)}),!1}}async start(){try{if(n.info("Starting CodeVibe MCP Server...",{environment:(0,a.getEnvironment)()}),this.appSyncClient=new a.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()){n.info("Authenticated with stored OAuth tokens",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,a.registerDeviceEncryptionKey)(this.appSyncClient,n),(0,a.startDeviceKeyWatcher)(this.appSyncClient,n);try{let s=await this.appSyncClient.sweepOrphanSessions({agentType:"CLAUDE"});s>0&&n.info("Orphan sweep: marked stale Claude sessions INACTIVE",{swept:s})}catch(s){n.warn("Orphan sweep failed, continuing startup",{error:s instanceof Error?s.message:String(s)})}}else n.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),n.info("MCP Server started successfully",{port:this.assignedPort,host:(0,a.getConfig)().server.host,dynamicPort:(0,a.getConfig)().server.dynamicPort,sessionId:this.initialSessionId,authenticated:this.appSyncClient.isAuthenticated(),userId:this.appSyncClient.getCurrentUserId()})}catch(e){throw n.error("Failed to start MCP Server:",e),e}}async stop(){n.info("Stopping MCP Server...");let e=Array.from(this.activeSessions.keys()),s=new Set;n.info(`Marking ${e.length} active session(s) as INACTIVE...`);for(let t of e){let i=this.activeSessions.get(t);i?.mobileEndWatcher&&(i.mobileEndWatcher.stop(),i.mobileEndWatcher=void 0)}for(let t of e)try{let i=this.activeSessions.get(t);if(i&&this.hasOtherLiveDaemonForSession(i.claudeSessionId)){n.info("Another daemon serves this session \u2014 skipping mark INACTIVE AND port file removal during shutdown",{sessionId:t,claudeSessionId:i.claudeSessionId,myPid:process.pid}),s.add(i.claudeSessionId);continue}await this.appSyncClient.updateSession({sessionId:t,status:a.SessionStatus.INACTIVE}),n.info("Session marked as INACTIVE during shutdown",{sessionId:t}),i&&this.removePortFile(i.claudeSessionId)}catch(i){n.warn("Failed to mark session as INACTIVE during shutdown",{sessionId:t,error:i})}this.appSyncClient.cleanupSubscriptions(),this.activeSessions.clear(),await this.httpApi.stop({protectedSessionIds:s}),n.info("MCP Server stopped")}async handleEventFromHook(e){let{session_id:s,hook_event_name:t,type:i,content:r}=e;n.info("Processing hook event",{sessionId:s,hookEvent:t,type:i});try{t==="SessionStart"?await this.handleSessionStart(e):t==="SessionEnd"&&await this.handleSessionEnd(e);let o=this.claudeToBackendSessionId.get(s)||this.generateBackendSessionId(s);if(i===a.EventType.USER_PROMPT&&e.source===a.EventSource.DESKTOP&&t==="UserPromptSubmit"&&r&&this.isRecentMobilePrompt(o,r)){n.info("Skipping duplicate USER_PROMPT from mobile-originated prompt",{sessionId:o,contentLength:r.length});return}if(i===a.EventType.INTERACTIVE_PROMPT){let h=this.activeSessions.get(o);h&&(h.waitingForPromptResponse=!0,h.pendingPromptId=e.prompt_id,n.info("Interactive prompt detected - will parse options from tmux",{sessionId:o,promptId:e.prompt_id})),this.sendInteractivePromptAsync(o,e,r).catch(g=>{n.error("Failed to send interactive prompt with dynamic options",{error:g})});return}let c=r,m=e.metadata,p=!1;n.info("Hook event encryption state",{type:i,sessionId:o,hasSessionKey:!!this.sessionKey,sessionKeyLength:this.sessionKey?.length||0}),this.sessionKey?(c=a.cryptoService.encryptContent(r,this.sessionKey),m&&(m={encrypted:a.cryptoService.encryptMetadata(m,this.sessionKey)}),p=!0,n.info("Event encrypted for hook",{type:i,sessionId:o,isEncrypted:!0})):n.warn("No session key - event will NOT be encrypted",{type:i,sessionId:o});let d=await this.appSyncClient.createEvent({sessionId:o,type:i,source:e.source,content:c,metadata:m,promptId:e.prompt_id,isEncrypted:p?!0:void 0});if(i===a.EventType.USER_PROMPT&&e.source===a.EventSource.DESKTOP){let h=this.activeSessions.get(o);h?.waitingForPromptResponse&&(h.waitingForPromptResponse=!1,h.pendingPromptId=void 0,h.pendingSubmitMap=void 0,n.info("Clearing prompt wait state - new desktop prompt received",{sessionId:o}))}n.debug("Event sent to AppSync successfully")}catch(o){throw n.error("Failed to process hook event:",o),o}}async handleSessionStart(e){let s=e.session_id,t=this.generateBackendSessionId(s),i=e.metadata?.cwd||process.cwd();this.claudeToBackendSessionId.set(s,t),n.info("Session started",{claudeSessionId:s,sessionId:t,cwd:i});let r=Array.from(this.activeSessions.keys()).filter(p=>p!==t);if(r.length>0){n.info(`Marking ${r.length} previous session(s) as INACTIVE`);for(let p of r){let d=this.activeSessions.get(p);d?.mobileEndWatcher&&(d.mobileEndWatcher.stop(),d.mobileEndWatcher=void 0);try{await this.appSyncClient.updateSession({sessionId:p,status:a.SessionStatus.INACTIVE}),n.info("Previous session marked INACTIVE",{prevId:p,newSessionId:t})}catch(h){n.warn("Failed to mark previous session as INACTIVE",{prevId:p,error:h})}d&&this.removePortFile(d.claudeSessionId),this.activeSessions.delete(p)}}this.writePortFile(s);let o=this.appSyncClient.getCurrentUserId(),c={sessionId:t,claudeSessionId:s,userId:o,projectPath:i,cwd:i,createdAt:new Date,subscriptionActive:!1,waitingForPromptResponse:!1,metadata:e.metadata||{}};this.activeSessions.set(t,c);try{let p=await(0,a.resumeOrCreateSession)({sessionId:t,userId:c.userId,agentType:a.AgentType.CLAUDE,projectPath:i,metadata:e.metadata||{}},this.appSyncClient,n);if(this.sessionKey=p.sessionKey,p.resumed&&!p.sessionKey){let d=await a.keychainManager.getDeviceId();n.error("Device key not found in session encryptedKeys",{sessionId:t,pluginDeviceId:d}),console.error(`
4
- \u26A0\uFE0F E2E ENCRYPTION WARNING: Cannot decrypt this session!`),console.error(` Your device ID (${d.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(p){if(this.isSessionLimitExceeded(p)){this.displaySubscriptionLimitError(p,"session"),this.activeSessions.delete(t),this.removePortFile(s);return}n.error("Failed to create/resume session:",p)}this.subscribeToMobileEvents(t),this.appSyncClient.startHeartbeat(t);let m=this.activeSessions.get(t);m&&(m.mobileEndWatcher=this.appSyncClient.watchForMobileEnd(t,async()=>{n.info("Mobile ended session \u2014 sending desktop quit",{sessionId:t});let p=process.env[B];if(!p){n.warn("No tmux session set; skipping desktop self-terminate",{sessionId:t,expectedEnv:B});return}await re(p,oe)}))}async handleSessionEnd(e){let s=e.session_id,t=this.claudeToBackendSessionId.get(s)||this.generateBackendSessionId(s);n.info("Session ended",{claudeSessionId:s,sessionId:t,reason:e.metadata?.reason});let i=this.activeSessions.get(t);if(i?.mobileEndWatcher&&(i.mobileEndWatcher.stop(),i.mobileEndWatcher=void 0),this.removePortFile(s),i?.waitingForPromptResponse&&(n.info("Clearing prompt wait state - session ending",{sessionId:t}),i.waitingForPromptResponse=!1,i.pendingPromptId=void 0),this.appSyncClient.stopHeartbeat(t),i)try{await this.appSyncClient.updateSession({sessionId:t,status:a.SessionStatus.INACTIVE}),n.info("Session marked as INACTIVE in AppSync",{sessionId:t})}catch(r){n.warn("Failed to update session in AppSync:",r)}else n.warn("Cannot update session - session state not found",{sessionId:t});this.activeSessions.delete(t),this.claudeToBackendSessionId.delete(s),n.debug("Session cleanup completed",{sessionId:t})}subscribeToMobileEvents(e){n.info("Subscribing to mobile events",{sessionId:e});let s=this.activeSessions.get(e);if(!s){n.error("Session not found",{sessionId:e});return}this.appSyncClient.subscribeToEvents(e,async t=>{n.info("Received mobile event",{eventId:t.eventId,type:t.type,sessionId:t.sessionId,isEncrypted:t.isEncrypted});let i=t.content||"";if(t.isEncrypted&&this.sessionKey)try{i=a.cryptoService.decryptContent(t.content,this.sessionKey),n.debug("Event decrypted successfully",{eventId:t.eventId})}catch(o){n.error("Failed to decrypt event:",{eventId:t.eventId,error:o}),i=t.content}let r={...t,content:i};try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:a.DeliveryStatus.DELIVERED}),n.info("Event marked as DELIVERED",{eventId:t.eventId})}catch(o){n.warn("Failed to mark event as DELIVERED",{eventId:t.eventId,error:o})}if(t.type===a.EventType.USER_PROMPT){let o=this.activeSessions.get(e);if(o?.waitingForPromptResponse){let c=i.trim(),m=o.pendingSubmitMap?Object.keys(o.pendingSubmitMap).length:3,p=this.parseInteractivePromptInput(c,m);if(n.info("Parsed interactive prompt input",{sessionId:e,content:c,parsed:p,hasSubmitMap:!!o.pendingSubmitMap}),p.action==="select_option"){let d=o.pendingSubmitMap?.[p.option]||p.option;n.info("User selected option",{option:p.option,terminalInput:d}),await this.promptResponder.answerInteractivePrompt(e,d)?(await this.markEventExecuted(t),o.waitingForPromptResponse=!1,o.pendingPromptId=void 0,o.pendingSubmitMap=void 0,await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:`Selected option ${p.option}`,metadata:{promptAnswered:!0}})):await this.sendPromptError(e,"Failed to select option")}else if(p.action==="option_with_followup"){let d=o.pendingSubmitMap?.[p.option]||p.option;n.info("User selected option with follow-up",{option:p.option,terminalInput:d,followUpText:p.followUpText});let h=await this.promptResponder.answerInteractivePrompt(e,d);if(o.waitingForPromptResponse=!1,o.pendingPromptId=void 0,o.pendingSubmitMap=void 0,h){if(await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:`Selected option ${p.option}`,metadata:{promptAnswered:!0}}),p.followUpText){await new Promise(f=>setTimeout(f,1e3));let g={...t,content:p.followUpText};await this.executeMobilePrompt(e,g)}await this.markEventExecuted(t)}else await this.sendPromptError(e,"Failed to select option")}else n.info("Sending as free-form response to interactive prompt",{response:c}),await this.promptResponder.answerInteractivePrompt(e,c)?(await this.markEventExecuted(t),o.waitingForPromptResponse=!1,o.pendingPromptId=void 0,o.pendingSubmitMap=void 0,await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:"Response sent to interactive prompt",metadata:{promptAnswered:!0}})):await this.sendPromptError(e,"Failed to send response")}else await this.executeMobilePrompt(e,r)}},t=>{n.error("Subscription error",{sessionId:e,error:t})}),s.subscriptionActive=!0,n.info("Subscription active",{sessionId:e})}async sendInteractivePromptAsync(e,s,t){await new Promise(d=>setTimeout(d,500));let i=process.env.CODEVIBE_TMUX_SESSION,r={...s.metadata||{}};if(i)try{let{exec:d}=await import("child_process"),h=x=>new Promise((G,Y)=>{d(x,{timeout:5e3},(_,z)=>{_?Y(_):G({stdout:z||""})})}),{stdout:g}=await h(`tmux capture-pane -p -e -S -30 -t '${i}'`),f=g.split(`
6
- `);n.info("tmux capture result",{tmuxSession:i,totalLines:f.length,lastLines:f.slice(-15).map(x=>x.replace(/\x1B[^m]*m/g,"").trim()).filter(Boolean)});let S=(0,a.parseInteractivePrompt)(g);S&&S.options.length>0?(r.options=S.options,r.submitMap=S.submitMap,r.instructions=this.buildPromptInstructions(S),n.info("Parsed dynamic options from tmux",{optionCount:S.options.length,kind:S.kind,options:S.options})):(n.info("No dynamic options parsed from tmux, using fallback",{parsedResult:S}),this.addFallbackOptions(r))}catch(d){n.warn("Failed to capture tmux pane for options",{error:d}),this.addFallbackOptions(r)}else n.warn("No tmux session \u2014 using fallback options"),this.addFallbackOptions(r);let o=this.activeSessions.get(e);o&&r.submitMap&&(o.pendingSubmitMap=r.submitMap);let c=t,m=r,p=!1;this.sessionKey&&(c=a.cryptoService.encryptContent(t,this.sessionKey),m={encrypted:a.cryptoService.encryptMetadata(m,this.sessionKey)},p=!0),await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.INTERACTIVE_PROMPT,source:s.source,content:c,metadata:m,promptId:s.prompt_id,isEncrypted:p?!0:void 0}),n.info("Interactive prompt sent to AppSync with dynamic options",{sessionId:e})}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(t=>t.number).join(", ")}. Append a message to provide alternative instructions.`}parseInteractivePromptInput(e,s=3){return X(e,s)}async markEventExecuted(e){try{await this.appSyncClient.updateEventStatus({eventId:e.eventId,sessionId:e.sessionId,timestamp:e.timestamp,deliveryStatus:a.DeliveryStatus.EXECUTED}),n.info("Event marked as EXECUTED",{eventId:e.eventId})}catch(s){n.warn("Failed to mark event as EXECUTED",{eventId:e.eventId,error:s})}}async sendPromptError(e,s){await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:s,metadata:{error:!0}})}isSessionLimitExceeded(e){return this.getErrorMessage(e).includes("SESSION_LIMIT_EXCEEDED")}isUsageLimitExceeded(e){let s=this.getErrorMessage(e);return s.includes("MESSAGE_LIMIT_EXCEEDED")||s.includes("IMAGE_LIMIT_EXCEEDED")}getErrorMessage(e){if(e instanceof Error)return e.message;if(typeof e=="object"&&e!==null){let s=e;if(s.errors&&Array.isArray(s.errors))return s.errors.map(t=>t.message||"").join(" ");if(typeof s.message=="string")return s.message}return String(e)}displaySubscriptionLimitError(e,s){let t=this.getErrorMessage(e),i="",r=t.match(/for your (\w+) plan/i);r&&(i=` (${r[1]} tier)`);let o="",c=t.match(/of (\d+)/);switch(c&&(o=` [Limit: ${c[1]}]`),console.log(`
7
- `+"=".repeat(60)),console.log("\u26A0\uFE0F SUBSCRIPTION LIMIT REACHED"),console.log("=".repeat(60)),s){case"session":console.log(`You have reached the maximum number of active sessions${i}.`),console.log(`${o}`),console.log(`
8
- 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${i}.`),console.log(`${o}`),console.log(`
9
- 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${i}.`),console.log(`${o}`),console.log(`
1
+ "use strict";var ee=Object.create;var C=Object.defineProperty;var te=Object.getOwnPropertyDescriptor;var se=Object.getOwnPropertyNames;var ne=Object.getPrototypeOf,ie=Object.prototype.hasOwnProperty;var oe=(l,e)=>{for(var s in e)C(l,s,{get:e[s],enumerable:!0})},N=(l,e,s,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of se(e))!ie.call(l,i)&&i!==s&&C(l,i,{get:()=>e[i],enumerable:!(t=te(e,i))||t.enumerable});return l};var v=(l,e,s)=>(s=l!=null?ee(ne(l)):{},N(e||!l||!l.__esModule?C(s,"default",{value:l,enumerable:!0}):s,l)),re=l=>N(C({},"__esModule",{value:!0}),l);var le={};oe(le,{parseInteractivePromptInput:()=>z});module.exports=re(le);var S=v(require("fs")),w=v(require("path")),R=v(require("os")),X=require("child_process"),G=require("util"),Y=require("child_process");var $=v(require("os")),U=v(require("path")),K=require("@quantiya/codevibe-core"),n=(0,K.createLogger)({name:"codevibe-claude",logFile:U.default.join($.default.tmpdir(),"codevibe-claude-mcp.log"),level:"info"});var a=require("@quantiya/codevibe-core");var A=v(require("express")),P=v(require("fs")),_=v(require("path")),F=v(require("os")),L=require("@quantiya/codevibe-core");var h=require("@quantiya/codevibe-core");var T=class{constructor(){this.assignedPort=0;this.app=(0,A.default)(),this.setupMiddleware(),this.setupRoutes()}setSessionId(e){this.sessionId=e}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(A.default.json({limit:"1mb"})),this.app.use((e,s,t)=>{n.debug(`${e.method} ${e.path}`,{body:e.body,query:e.query}),t()}),this.app.use((e,s,t,i)=>{n.error("Express error:",e);let o={success:!1,error:e.message||"Internal server error"};t.status(500).json(o)})}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,s){let t={success:!0,data:{status:"healthy",uptime:process.uptime(),version:"0.1.0",timestamp:new Date().toISOString()}};s.json(t)}async handleEvent(e,s){try{let t=e.body;if(!t.session_id){let r={success:!1,error:"Missing required field: session_id"};s.status(400).json(r);return}if(!t.hook_event_name){let r={success:!1,error:"Missing required field: hook_event_name"};s.status(400).json(r);return}let i=this.transformHookToEvent(t);n.info("Received event from hook",{sessionId:t.session_id,hookEvent:t.hook_event_name,type:i.type}),this.eventHandler?await this.eventHandler(i):n.warn("No event handler registered");let o={success:!0,message:"Event processed successfully"};s.json(o)}catch(t){n.error("Error handling event:",t);let i={success:!1,error:t instanceof Error?t.message:"Unknown error"};s.status(500).json(i)}}async handleTestExecute(e,s){try{let{sessionId:t,prompt:i}=e.body;if(!t||!i){let r={success:!1,error:"Missing required fields: sessionId, prompt"};s.status(400).json(r);return}n.info("Test execute request",{sessionId:t,prompt:i});let o={success:!0,message:"Test execution endpoint - not implemented yet",data:{sessionId:t,prompt:i}};s.json(o)}catch(t){n.error("Error in test execute:",t);let i={success:!1,error:t instanceof Error?t.message:"Unknown error"};s.status(500).json(i)}}transformHookToEvent(e){let s,t,i={cwd:e.cwd,hook_event_name:e.hook_event_name,...e.metadata||{}};if(e.type&&e.content!==void 0)s=e.type,t=e.content;else switch(e.hook_event_name){case"SessionStart":s=h.EventType.NOTIFICATION,t="Session started",i.source=e.source;break;case"SessionEnd":s=h.EventType.NOTIFICATION,t=`Session ended: ${e.reason||"unknown"}`,i.reason=e.reason;break;case"UserPromptSubmit":s=h.EventType.USER_PROMPT,t=e.prompt||"";break;case"PostToolUse":s=h.EventType.TOOL_USE,t=JSON.stringify({tool_name:e.tool_name,tool_input:e.tool_input,tool_response:e.tool_response}),i.tool_name=e.tool_name;break;case"Notification":s=h.EventType.NOTIFICATION,t=e.message||"",i.notification_type=e.notification_type;break;default:s=h.EventType.NOTIFICATION,t=`Hook event: ${e.hook_event_name}`}return{session_id:e.session_id,hook_event_name:e.hook_event_name,type:s,source:h.EventSource.DESKTOP,content:t,metadata:i}}onEvent(e){this.eventHandler=e}async start(e){let s=e||this.sessionId;return s&&(this.sessionId=s),new Promise((t,i)=>{try{let o=(0,L.getConfig)(),r=o.server.dynamicPort?0:o.server.port;this.server=this.app.listen(r,o.server.host,()=>{let c=this.server.address();this.assignedPort=c.port,n.info(`HTTP API listening on http://${o.server.host}:${this.assignedPort}`),this.sessionId&&this.writePortFile(this.sessionId,this.assignedPort),t(this.assignedPort)}),this.server.on("error",c=>{n.error("HTTP server error:",c),i(c)})}catch(o){i(o)}})}writePortFile(e,s){let t=_.join(F.tmpdir(),`codevibe-claude-${e}.port`);try{P.writeFileSync(t,s.toString()),n.info(`Port file written: ${t} -> ${s}`)}catch(i){n.error(`Failed to write port file: ${t}`,i)}}removePortFile(){if(this.sessionId){let e=_.join(F.tmpdir(),`codevibe-claude-${this.sessionId}.port`);try{P.existsSync(e)&&(P.unlinkSync(e),n.info(`Port file removed: ${e}`))}catch(s){n.warn(`Failed to remove port file: ${e}`,s)}}}async stop(e){return new Promise((s,t)=>{this.sessionId&&e?.protectedSessionIds?.has(this.sessionId)?n.info("Skipping port file removal \u2014 another daemon still serves this session",{sessionId:this.sessionId}):this.removePortFile(),this.server?this.server.close(i=>{i?(n.error("Error stopping HTTP server:",i),t(i)):(n.info("HTTP API stopped"),s())}):s()})}};var j=require("child_process"),V=require("@quantiya/codevibe-core");var x=class{async executePrompt(e,s){let t=(0,V.getConfig)(),i=t.claude.defaultTimeout;return n.info("Executing prompt from mobile",{sessionId:e,promptLength:s.length,timeout:i}),new Promise(o=>{let r=["--resume",e,"--print","--output-format","stream-json",s];n.debug("Spawning Claude command",{command:t.claude.command,args:r});let c=(0,j.spawn)(t.claude.command,r,{stdio:["pipe","pipe","pipe"],shell:!0}),d="",p="",m=!1,g=setTimeout(()=>{m=!0,n.warn("Command execution timed out",{sessionId:e,timeout:i}),c.kill("SIGTERM")},i);c.stdout?.on("data",f=>{let u=f.toString();d+=u,n.debug("Command stdout",{output:u.slice(0,200)})}),c.stderr?.on("data",f=>{let u=f.toString();p+=u,n.debug("Command stderr",{output:u.slice(0,200)})}),c.on("close",f=>{clearTimeout(g);let u={success:f===0&&!m,output:d,error:p,exitCode:f||void 0,timedOut:m};u.success?n.info("Command executed successfully",{sessionId:e,exitCode:f,outputLength:d.length}):n.error("Command execution failed",{sessionId:e,exitCode:f,timedOut:m,error:p.slice(0,500)}),o(u)}),c.on("error",f=>{clearTimeout(g),n.error("Failed to spawn command",{error:f.message}),o({success:!1,error:f.message,timedOut:!1})})})}detectInteractivePrompt(e){return[/\[Y\/n\]/i,/\[y\/N\]/i,/\(y\/n\)/i,/Continue\?/i,/Proceed\?/i].some(t=>t.test(e))}extractPromptText(e){let s=e.split(`
2
+ `);for(let t=s.length-1;t>=0;t--){let i=s[t].trim();if(this.detectInteractivePrompt(i))return i}return null}};var H=require("child_process"),q=require("util");var B=(0,q.promisify)(H.exec),k=class{async answerInteractivePrompt(e,s){n.info("Attempting to answer interactive prompt",{sessionId:e,response:s});try{let t=process.env.CODEVIBE_TMUX_SESSION;return n.info("Checking tmux session environment",{tmuxSession:t||"(not set)",allEnvKeys:Object.keys(process.env).filter(i=>i.includes("CODEVIBE")||i.includes("TMUX"))}),t?(n.info("Using tmux send-keys",{tmuxSession:t}),await this.sendViaTmux(t,s),n.info("Successfully sent response to interactive prompt",{sessionId:e,response:s}),!0):(n.error("No tmux session found - codevibe-claude wrapper is required",{sessionId:e,hint:"Start Claude Code using the codevibe-claude wrapper script"}),!1)}catch(t){return n.error("Failed to answer interactive prompt",{sessionId:e,error:t instanceof Error?t.message:String(t)}),!1}}async sendViaTmux(e,s){let t=s.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");n.info("Sending via tmux",{sessionName:e,inputLength:s.length});try{let i=`tmux send-keys -t "${e}" -l "${t}"`,o=await B(i);n.info("tmux send-keys (text) completed",{stdout:o.stdout||"(empty)",stderr:o.stderr||"(empty)"}),await this.delay(500);let r=`tmux send-keys -t "${e}" Enter`,c=await B(r);n.info("tmux send-keys (Enter) completed",{stdout:c.stdout||"(empty)",stderr:c.stderr||"(empty)"})}catch(i){throw n.error("tmux send-keys failed",{sessionName:e,error:i}),i}}delay(e){return new Promise(s=>setTimeout(s,e))}isPromptResponse(e){let s=e.trim().toLowerCase();return!!(s==="y"||s==="n"||s==="yes"||s==="no"||/^[0-9]+$/.test(s)||/^[a-z]$/.test(s)||["exit","quit","q","continue","skip","abort","retry","cancel"].includes(s))}};var ae=(0,G.promisify)(Y.exec),ce="/exit",W="CODEVIBE_TMUX_SESSION";async function pe(l,e){let s=async(t,i)=>{try{await ae(t)}catch(o){n.warn("tmux send-keys failed during self-terminate",{sessionName:l,label:i,error:String(o)})}};await s(`tmux send-keys -t "${l}" C-c`,"ctrl-c"),await new Promise(t=>setTimeout(t,200)),await s(`tmux send-keys -t "${l}" -l "${e}"`,"quit-text"),await new Promise(t=>setTimeout(t,500)),await s(`tmux send-keys -t "${l}" Enter`,"enter")}var O=class l{constructor(e){this.activeSessions=new Map;this.assignedPort=0;this.sessionKey=null;this.claudeToBackendSessionId=new Map;this.pendingMobilePrompts=new Map;this.httpApi=new T,this.commandExecutor=new x,this.promptResponder=new k,this.initialSessionId=e}static{this.MOBILE_PROMPT_EXPIRY_MS=3e3}getPort(){return this.assignedPort}generateBackendSessionId(e){return`claude-${e}`}trackMobilePrompt(e,s){this.pendingMobilePrompts.has(e)||this.pendingMobilePrompts.set(e,[]),this.pendingMobilePrompts.get(e).push({prompt:s.trim(),timestamp:Date.now()}),n.debug("Tracking mobile prompt for deduplication",{sessionId:e,promptLength:s.length})}isRecentMobilePrompt(e,s){let t=this.pendingMobilePrompts.get(e);if(!t)return!1;let i=Date.now(),o=s.trim(),r=[],c=!1;for(let d of t)if(!(i-d.timestamp>l.MOBILE_PROMPT_EXPIRY_MS)){if(!c&&d.prompt===o){c=!0,n.debug("Found matching mobile prompt, filtering duplicate",{sessionId:e});continue}r.push(d)}return r.length>0?this.pendingMobilePrompts.set(e,r):this.pendingMobilePrompts.delete(e),c}writePortFile(e){let s=w.join(R.tmpdir(),`codevibe-claude-${e}.port`);try{S.writeFileSync(s,this.assignedPort.toString()),n.info(`Port file written: ${s} -> ${this.assignedPort}`)}catch(t){n.error(`Failed to write port file: ${s}`,t)}}removePortFile(e){let s=w.join(R.tmpdir(),`codevibe-claude-${e}.port`);try{S.existsSync(s)&&(S.unlinkSync(s),n.info(`Port file removed: ${s}`))}catch(t){n.warn(`Failed to remove port file: ${s}`,t)}}hasOtherLiveDaemonForSession(e){try{let s=(0,X.execSync)("ps -eww -o pid= -o args=",{encoding:"utf8",timeout:2e3}),t=process.pid;for(let i of s.split(`
3
+ `)){let o=i.trim();if(!o)continue;let r=o.indexOf(" ");if(r<0)continue;let c=parseInt(o.substring(0,r),10);if(isNaN(c)||c===t)continue;let d=o.substring(r+1);if(/node.*codevibe-claude.*server\.js/.test(d)&&d.includes(e))return!0}return!1}catch(s){return n.warn('hasOtherLiveDaemonForSession: ps query failed; falling back to "no other daemon"',{error:String(s)}),!1}}async start(){try{if(n.info("Starting CodeVibe MCP Server...",{environment:(0,a.getEnvironment)()}),this.appSyncClient=new a.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()){n.info("Authenticated with stored OAuth tokens",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,a.registerDeviceEncryptionKey)(this.appSyncClient,n),(0,a.startDeviceKeyWatcher)(this.appSyncClient,n);try{let s=await this.appSyncClient.sweepOrphanSessions({agentType:"CLAUDE"});s>0&&n.info("Orphan sweep: marked stale Claude sessions INACTIVE",{swept:s})}catch(s){n.warn("Orphan sweep failed, continuing startup",{error:s instanceof Error?s.message:String(s)})}}else n.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),n.info("MCP Server started successfully",{port:this.assignedPort,host:(0,a.getConfig)().server.host,dynamicPort:(0,a.getConfig)().server.dynamicPort,sessionId:this.initialSessionId,authenticated:this.appSyncClient.isAuthenticated(),userId:this.appSyncClient.getCurrentUserId()})}catch(e){throw n.error("Failed to start MCP Server:",e),e}}async stop(){n.info("Stopping MCP Server...");let e=Array.from(this.activeSessions.keys()),s=new Set;n.info(`Marking ${e.length} active session(s) as INACTIVE...`);for(let t of e){let i=this.activeSessions.get(t);i?.mobileEndWatcher&&(i.mobileEndWatcher.stop(),i.mobileEndWatcher=void 0)}for(let t of e)try{let i=this.activeSessions.get(t);if(i&&this.hasOtherLiveDaemonForSession(i.claudeSessionId)){n.info("Another daemon serves this session \u2014 skipping mark INACTIVE AND port file removal during shutdown",{sessionId:t,claudeSessionId:i.claudeSessionId,myPid:process.pid}),s.add(i.claudeSessionId);continue}await this.appSyncClient.updateSession({sessionId:t,status:a.SessionStatus.INACTIVE}),n.info("Session marked as INACTIVE during shutdown",{sessionId:t}),i&&this.removePortFile(i.claudeSessionId)}catch(i){n.warn("Failed to mark session as INACTIVE during shutdown",{sessionId:t,error:i})}this.appSyncClient.cleanupSubscriptions(),this.activeSessions.clear(),await this.httpApi.stop({protectedSessionIds:s}),n.info("MCP Server stopped")}async handleEventFromHook(e){let{session_id:s,hook_event_name:t,type:i,content:o}=e;n.info("Processing hook event",{sessionId:s,hookEvent:t,type:i});try{t==="SessionStart"?await this.handleSessionStart(e):t==="SessionEnd"&&await this.handleSessionEnd(e);let r=this.claudeToBackendSessionId.get(s)||this.generateBackendSessionId(s);if(i===a.EventType.USER_PROMPT&&e.source===a.EventSource.DESKTOP&&t==="UserPromptSubmit"&&o&&this.isRecentMobilePrompt(r,o)){n.info("Skipping duplicate USER_PROMPT from mobile-originated prompt",{sessionId:r,contentLength:o.length});return}if(i===a.EventType.INTERACTIVE_PROMPT){let g=this.activeSessions.get(r);g&&(g.waitingForPromptResponse=!0,g.pendingPromptId=e.prompt_id,n.info("Interactive prompt detected - will parse options from tmux",{sessionId:r,promptId:e.prompt_id})),this.sendInteractivePromptAsync(r,e,o).catch(f=>{n.error("Failed to send interactive prompt with dynamic options",{error:f})});return}let c=o,d=e.metadata,p=!1;n.info("Hook event encryption state",{type:i,sessionId:r,hasSessionKey:!!this.sessionKey,sessionKeyLength:this.sessionKey?.length||0}),this.sessionKey?(c=a.cryptoService.encryptContent(o,this.sessionKey),d&&(d={encrypted:a.cryptoService.encryptMetadata(d,this.sessionKey)}),p=!0,n.info("Event encrypted for hook",{type:i,sessionId:r,isEncrypted:!0})):n.warn("No session key - event will NOT be encrypted",{type:i,sessionId:r});let m=await this.appSyncClient.createEvent({sessionId:r,type:i,source:e.source,content:c,metadata:d,promptId:e.prompt_id,isEncrypted:p?!0:void 0});if(i===a.EventType.USER_PROMPT&&e.source===a.EventSource.DESKTOP){let g=this.activeSessions.get(r);g?.waitingForPromptResponse&&(g.waitingForPromptResponse=!1,g.pendingPromptId=void 0,g.pendingSubmitMap=void 0,n.info("Clearing prompt wait state - new desktop prompt received",{sessionId:r}))}n.debug("Event sent to AppSync successfully")}catch(r){throw n.error("Failed to process hook event:",r),r}}async handleSessionStart(e){let s=e.session_id,t=this.generateBackendSessionId(s),i=e.metadata?.cwd||process.cwd();this.claudeToBackendSessionId.set(s,t),n.info("Session started",{claudeSessionId:s,sessionId:t,cwd:i});let o=Array.from(this.activeSessions.keys()).filter(p=>p!==t);if(o.length>0){n.info(`Marking ${o.length} previous session(s) as INACTIVE`);for(let p of o){let m=this.activeSessions.get(p);m?.mobileEndWatcher&&(m.mobileEndWatcher.stop(),m.mobileEndWatcher=void 0);try{await this.appSyncClient.updateSession({sessionId:p,status:a.SessionStatus.INACTIVE}),n.info("Previous session marked INACTIVE",{prevId:p,newSessionId:t})}catch(g){n.warn("Failed to mark previous session as INACTIVE",{prevId:p,error:g})}m&&this.removePortFile(m.claudeSessionId),this.activeSessions.delete(p)}}this.writePortFile(s);let r=this.appSyncClient.getCurrentUserId(),c={sessionId:t,claudeSessionId:s,userId:r,projectPath:i,cwd:i,createdAt:new Date,subscriptionActive:!1,waitingForPromptResponse:!1,metadata:e.metadata||{}};this.activeSessions.set(t,c);try{let p=await(0,a.resumeOrCreateSession)({sessionId:t,userId:c.userId,agentType:a.AgentType.CLAUDE,projectPath:i,metadata:e.metadata||{}},this.appSyncClient,n);if(this.sessionKey=p.sessionKey,p.resumed&&!p.sessionKey){let m=await a.keychainManager.getDeviceId();n.error("Device key not found in session encryptedKeys",{sessionId:t,pluginDeviceId:m}),console.error(`
4
+ \u26A0\uFE0F E2E ENCRYPTION WARNING: Cannot decrypt this session!`),console.error(` Your device ID (${m.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(p){if(this.isSessionLimitExceeded(p)){this.displaySubscriptionLimitError(p,"session"),this.activeSessions.delete(t),this.removePortFile(s);return}n.error("Failed to create/resume session:",p)}this.subscribeToMobileEvents(t),this.appSyncClient.startHeartbeat(t);let d=this.activeSessions.get(t);d&&(d.mobileEndWatcher=this.appSyncClient.watchForMobileEnd(t,async()=>{n.info("Mobile ended session \u2014 sending desktop quit",{sessionId:t});let p=process.env[W];if(!p){n.warn("No tmux session set; skipping desktop self-terminate",{sessionId:t,expectedEnv:W});return}await pe(p,ce)}))}async handleSessionEnd(e){let s=e.session_id,t=this.claudeToBackendSessionId.get(s)||this.generateBackendSessionId(s);n.info("Session ended",{claudeSessionId:s,sessionId:t,reason:e.metadata?.reason});let i=this.activeSessions.get(t);if(i?.mobileEndWatcher&&(i.mobileEndWatcher.stop(),i.mobileEndWatcher=void 0),this.removePortFile(s),i?.waitingForPromptResponse&&(n.info("Clearing prompt wait state - session ending",{sessionId:t}),i.waitingForPromptResponse=!1,i.pendingPromptId=void 0),this.appSyncClient.stopHeartbeat(t),i)try{await this.appSyncClient.updateSession({sessionId:t,status:a.SessionStatus.INACTIVE}),n.info("Session marked as INACTIVE in AppSync",{sessionId:t})}catch(o){n.warn("Failed to update session in AppSync:",o)}else n.warn("Cannot update session - session state not found",{sessionId:t});this.activeSessions.delete(t),this.claudeToBackendSessionId.delete(s),n.debug("Session cleanup completed",{sessionId:t})}subscribeToMobileEvents(e){n.info("Subscribing to mobile events",{sessionId:e});let s=this.activeSessions.get(e);if(!s){n.error("Session not found",{sessionId:e});return}this.appSyncClient.subscribeToEvents(e,async t=>{n.info("Received mobile event",{eventId:t.eventId,type:t.type,sessionId:t.sessionId,isEncrypted:t.isEncrypted});let i=t.content||"";if(t.isEncrypted&&this.sessionKey)try{i=a.cryptoService.decryptContent(t.content,this.sessionKey),n.debug("Event decrypted successfully",{eventId:t.eventId})}catch(r){n.error("Failed to decrypt event:",{eventId:t.eventId,error:r}),i=t.content}let o={...t,content:i};try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:a.DeliveryStatus.DELIVERED}),n.info("Event marked as DELIVERED",{eventId:t.eventId})}catch(r){n.warn("Failed to mark event as DELIVERED",{eventId:t.eventId,error:r})}if(t.type===a.EventType.USER_PROMPT){let r=this.activeSessions.get(e);if(r?.waitingForPromptResponse){let c=i.trim(),d=r.pendingSubmitMap?Object.keys(r.pendingSubmitMap).length:3,p=this.parseInteractivePromptInput(c,d);if(n.info("Parsed interactive prompt input",{sessionId:e,content:c,parsed:p,hasSubmitMap:!!r.pendingSubmitMap}),p.action==="select_option"){let m=r.pendingSubmitMap?.[p.option]||p.option;n.info("User selected option",{option:p.option,terminalInput:m}),await this.promptResponder.answerInteractivePrompt(e,m)?(await this.markEventExecuted(t),r.waitingForPromptResponse=!1,r.pendingPromptId=void 0,r.pendingSubmitMap=void 0,await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:`Selected option ${p.option}`,metadata:{promptAnswered:!0}})):await this.sendPromptError(e,"Failed to select option")}else if(p.action==="option_with_followup"){let m=r.pendingSubmitMap?.[p.option]||p.option;n.info("User selected option with follow-up",{option:p.option,terminalInput:m,followUpText:p.followUpText});let g=await this.promptResponder.answerInteractivePrompt(e,m);if(r.waitingForPromptResponse=!1,r.pendingPromptId=void 0,r.pendingSubmitMap=void 0,g){if(await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:`Selected option ${p.option}`,metadata:{promptAnswered:!0}}),p.followUpText){await new Promise(u=>setTimeout(u,1e3));let f={...t,content:p.followUpText};await this.executeMobilePrompt(e,f)}await this.markEventExecuted(t)}else await this.sendPromptError(e,"Failed to select option")}else n.info("Sending as free-form response to interactive prompt",{response:c}),await this.promptResponder.answerInteractivePrompt(e,c)?(await this.markEventExecuted(t),r.waitingForPromptResponse=!1,r.pendingPromptId=void 0,r.pendingSubmitMap=void 0,await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:"Response sent to interactive prompt",metadata:{promptAnswered:!0}})):await this.sendPromptError(e,"Failed to send response")}else await this.executeMobilePrompt(e,o)}},t=>{n.error("Subscription error",{sessionId:e,error:t})}),s.subscriptionActive=!0,n.info("Subscription active",{sessionId:e})}async sendInteractivePromptAsync(e,s,t){await new Promise(u=>setTimeout(u,500));let i=process.env.CODEVIBE_TMUX_SESSION,o={...s.metadata||{}},r=s.metadata?.tool_name,c=s.metadata?.tool_input,d=r==="AskUserQuestion"?c?.questions?.[0]:null;if(d&&Array.isArray(d.options)&&d.options.length>0){let u=d.options.map((E,y)=>({number:String(y+1),text:typeof E=="string"?E:E.label||""})),I=String(u.length+1),b=String(u.length+2);u.push({number:I,text:"Type something"},{number:b,text:"Chat about this"}),o.options=u,o.submitMap=Object.fromEntries(u.map(E=>[E.number,E.number])),o.instructions=d.multiSelect?`Reply with comma-separated numbers (e.g., 1,3) for "${d.header||d.question}"`:`Reply with the number of your choice. For option ${I} (Type something), reply "${I}, your answer".`,t=d.question,n.info("AskUserQuestion: using tool_input directly (skipped tmux parse)",{questionPreview:d.question.slice(0,80),optionCount:u.length,multiSelect:!!d.multiSelect})}else if(i)try{let{exec:u}=await import("child_process"),I=M=>new Promise((Q,J)=>{u(M,{timeout:5e3},(D,Z)=>{D?J(D):Q({stdout:Z||""})})}),{stdout:b}=await I(`tmux capture-pane -p -e -S -30 -t '${i}'`),E=b.split(`
6
+ `);n.info("tmux capture result",{tmuxSession:i,totalLines:E.length,lastLines:E.slice(-15).map(M=>M.replace(/\x1B[^m]*m/g,"").trim()).filter(Boolean)});let y=(0,a.parseInteractivePrompt)(b);y&&y.options.length>0?(o.options=y.options,o.submitMap=y.submitMap,o.instructions=this.buildPromptInstructions(y),n.info("Parsed dynamic options from tmux",{optionCount:y.options.length,kind:y.kind,options:y.options})):(n.info("No dynamic options parsed from tmux, using fallback",{parsedResult:y}),this.addFallbackOptions(o))}catch(u){n.warn("Failed to capture tmux pane for options",{error:u}),this.addFallbackOptions(o)}else n.warn("No tmux session \u2014 using fallback options"),this.addFallbackOptions(o);let p=this.activeSessions.get(e);p&&o.submitMap&&(p.pendingSubmitMap=o.submitMap);let m=t,g=o,f=!1;this.sessionKey&&(m=a.cryptoService.encryptContent(t,this.sessionKey),g={encrypted:a.cryptoService.encryptMetadata(g,this.sessionKey)},f=!0),await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.INTERACTIVE_PROMPT,source:s.source,content:m,metadata:g,promptId:s.prompt_id,isEncrypted:f?!0:void 0}),n.info("Interactive prompt sent to AppSync with dynamic options",{sessionId:e})}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(t=>t.number).join(", ")}. Append a message to provide alternative instructions.`}parseInteractivePromptInput(e,s=3){return z(e,s)}async markEventExecuted(e){try{await this.appSyncClient.updateEventStatus({eventId:e.eventId,sessionId:e.sessionId,timestamp:e.timestamp,deliveryStatus:a.DeliveryStatus.EXECUTED}),n.info("Event marked as EXECUTED",{eventId:e.eventId})}catch(s){n.warn("Failed to mark event as EXECUTED",{eventId:e.eventId,error:s})}}async sendPromptError(e,s){await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:s,metadata:{error:!0}})}isSessionLimitExceeded(e){return this.getErrorMessage(e).includes("SESSION_LIMIT_EXCEEDED")}isUsageLimitExceeded(e){let s=this.getErrorMessage(e);return s.includes("MESSAGE_LIMIT_EXCEEDED")||s.includes("IMAGE_LIMIT_EXCEEDED")}getErrorMessage(e){if(e instanceof Error)return e.message;if(typeof e=="object"&&e!==null){let s=e;if(s.errors&&Array.isArray(s.errors))return s.errors.map(t=>t.message||"").join(" ");if(typeof s.message=="string")return s.message}return String(e)}displaySubscriptionLimitError(e,s){let t=this.getErrorMessage(e),i="",o=t.match(/for your (\w+) plan/i);o&&(i=` (${o[1]} tier)`);let r="",c=t.match(/of (\d+)/);switch(c&&(r=` [Limit: ${c[1]}]`),console.log(`
7
+ `+"=".repeat(60)),console.log("\u26A0\uFE0F SUBSCRIPTION LIMIT REACHED"),console.log("=".repeat(60)),s){case"session":console.log(`You have reached the maximum number of active sessions${i}.`),console.log(`${r}`),console.log(`
8
+ 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${i}.`),console.log(`${r}`),console.log(`
9
+ 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${i}.`),console.log(`${r}`),console.log(`
10
10
  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(`
11
11
  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)+`
12
- `),n.error("Subscription limit exceeded",{limitType:s,errorMessage:t})}async downloadAttachment(e,s,t){try{let i=e.isEncrypted??t??!1;n.info("Downloading attachment - START",{id:e.id,type:e.type,filename:e.filename,s3Key:e.s3Key,attachmentIsEncrypted:e.isEncrypted,eventIsEncrypted:t,shouldDecrypt:i,hasSessionKey:!!this.sessionKey});let{downloadUrl:r}=await this.appSyncClient.getAttachmentDownloadUrl(e.s3Key),o=await fetch(r);if(!o.ok)throw new Error(`Failed to download attachment: ${o.status} ${o.statusText}`);let c=Buffer.from(await o.arrayBuffer());if(n.info("Attachment downloaded",{id:e.id,downloadedSize:c.length,first20Bytes:c.slice(0,20).toString("hex")}),n.info("Checking decryption conditions",{id:e.id,shouldDecrypt:i,hasSessionKey:!!this.sessionKey,willDecrypt:!!(i&&this.sessionKey)}),i&&this.sessionKey)try{n.info("Decrypting attachment",{id:e.id,encryptedSize:c.length}),c=a.cryptoService.decryptData(c,this.sessionKey),n.info("Attachment decrypted successfully",{id:e.id,decryptedSize:c.length,first20Bytes:c.slice(0,20).toString("hex")})}catch(f){throw n.error("Failed to decrypt attachment:",{id:e.id,error:f}),new Error("Failed to decrypt attachment")}else i&&!this.sessionKey?n.warn("Cannot decrypt attachment - no session key available",{id:e.id}):n.info("Skipping decryption - attachment not encrypted or no session key",{id:e.id,shouldDecrypt:i,hasSessionKey:!!this.sessionKey});let m=E.join(T.tmpdir(),"codevibe-claude",s);v.existsSync(m)||v.mkdirSync(m,{recursive:!0});let p="",d=e.filename;if(i&&e.filename&&this.sessionKey)try{d=a.cryptoService.decryptContent(e.filename,this.sessionKey)}catch{d=e.filename}if(d){let f=E.extname(d);f&&(p=f)}p||(p={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[e.type]||".bin");let h=`attachment-${e.id}${p}`,g=E.join(m,h);return v.writeFileSync(g,c),n.info("Attachment saved to temp file",{id:e.id,filePath:g,size:c.length,wasDecrypted:i&&!!this.sessionKey}),g}catch(i){return n.error("Failed to download attachment:",{id:e.id,error:i}),null}}async executeMobilePrompt(e,s){let t=s.content||"",i=s.attachments||[];n.info("Executing mobile prompt via tmux",{sessionId:e,promptLength:t.length,attachmentCount:i.length});let r=[];if(i.length>0){n.info("Downloading attachments for prompt",{count:i.length});for(let o of i){let c=await this.downloadAttachment(o,e,s.isEncrypted);c&&r.push(c)}if(r.length>0){let o=r.map(c=>`[Attached file: ${c}]`).join(`
13
- `);t?t=`${o}
12
+ `),n.error("Subscription limit exceeded",{limitType:s,errorMessage:t})}async downloadAttachment(e,s,t){try{let i=e.isEncrypted??t??!1;n.info("Downloading attachment - START",{id:e.id,type:e.type,filename:e.filename,s3Key:e.s3Key,attachmentIsEncrypted:e.isEncrypted,eventIsEncrypted:t,shouldDecrypt:i,hasSessionKey:!!this.sessionKey});let{downloadUrl:o}=await this.appSyncClient.getAttachmentDownloadUrl(e.s3Key),r=await fetch(o);if(!r.ok)throw new Error(`Failed to download attachment: ${r.status} ${r.statusText}`);let c=Buffer.from(await r.arrayBuffer());if(n.info("Attachment downloaded",{id:e.id,downloadedSize:c.length,first20Bytes:c.slice(0,20).toString("hex")}),n.info("Checking decryption conditions",{id:e.id,shouldDecrypt:i,hasSessionKey:!!this.sessionKey,willDecrypt:!!(i&&this.sessionKey)}),i&&this.sessionKey)try{n.info("Decrypting attachment",{id:e.id,encryptedSize:c.length}),c=a.cryptoService.decryptData(c,this.sessionKey),n.info("Attachment decrypted successfully",{id:e.id,decryptedSize:c.length,first20Bytes:c.slice(0,20).toString("hex")})}catch(u){throw n.error("Failed to decrypt attachment:",{id:e.id,error:u}),new Error("Failed to decrypt attachment")}else i&&!this.sessionKey?n.warn("Cannot decrypt attachment - no session key available",{id:e.id}):n.info("Skipping decryption - attachment not encrypted or no session key",{id:e.id,shouldDecrypt:i,hasSessionKey:!!this.sessionKey});let d=w.join(R.tmpdir(),"codevibe-claude",s);S.existsSync(d)||S.mkdirSync(d,{recursive:!0});let p="",m=e.filename;if(i&&e.filename&&this.sessionKey)try{m=a.cryptoService.decryptContent(e.filename,this.sessionKey)}catch{m=e.filename}if(m){let u=w.extname(m);u&&(p=u)}p||(p={"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}${p}`,f=w.join(d,g);return S.writeFileSync(f,c),n.info("Attachment saved to temp file",{id:e.id,filePath:f,size:c.length,wasDecrypted:i&&!!this.sessionKey}),f}catch(i){return n.error("Failed to download attachment:",{id:e.id,error:i}),null}}async executeMobilePrompt(e,s){let t=s.content||"",i=s.attachments||[];n.info("Executing mobile prompt via tmux",{sessionId:e,promptLength:t.length,attachmentCount:i.length});let o=[];if(i.length>0){n.info("Downloading attachments for prompt",{count:i.length});for(let r of i){let c=await this.downloadAttachment(r,e,s.isEncrypted);c&&o.push(c)}if(o.length>0){let r=o.map(c=>`[Attached file: ${c}]`).join(`
13
+ `);t?t=`${r}
14
14
 
15
- ${t}`:t=`${o}
15
+ ${t}`:t=`${r}
16
16
 
17
- Please analyze the attached file(s).`,n.info("Prompt updated with attachment paths",{attachmentCount:r.length,newPromptLength:t.length})}}this.trackMobilePrompt(e,t);try{if(await this.promptResponder.answerInteractivePrompt(e,t)){try{await this.appSyncClient.updateEventStatus({eventId:s.eventId,sessionId:s.sessionId,timestamp:s.timestamp,deliveryStatus:a.DeliveryStatus.EXECUTED}),n.info("Event marked as EXECUTED",{eventId:s.eventId})}catch(m){n.warn("Failed to mark event as EXECUTED",{eventId:s.eventId,error:m})}n.info("Mobile prompt sent successfully",{sessionId:e});let c=r.length>0?`Prompt with ${r.length} attachment(s) sent to Claude Code`:`Prompt "${t.substring(0,50)}${t.length>50?"...":""}" sent to Claude Code`;await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:c,metadata:{mobilePrompt:!0,attachmentCount:r.length}})}else n.error("Failed to send mobile prompt",{sessionId:e}),await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:"Failed to send prompt to Claude Code",metadata:{error:!0}})}catch(o){n.error("Failed to execute mobile prompt:",o)}}};async function ae(){let l=process.argv[2]||process.env.CLAUDE_SESSION_ID;l?n.info(`Starting MCP server for session: ${l}`):n.info("Starting MCP server without initial session ID (will be set on SessionStart)");let e=new A(l);try{await e.start();let s=e.getPort();console.log(`PORT=${s}`);let t=!1,i=async r=>{if(t){n.info("Shutdown already in progress, ignoring additional signal");return}t=!0,n.info(`Received ${r} signal, stopping server...`);try{await e.stop(),n.info("Graceful shutdown completed"),process.exit(0)}catch(o){n.error("Error during shutdown:",o),process.exit(1)}};process.on("SIGINT",()=>i("SIGINT")),process.on("SIGTERM",()=>i("SIGTERM")),process.on("SIGHUP",()=>i("SIGHUP")),process.on("uncaughtException",async r=>{n.error("Uncaught exception:",r),await i("uncaughtException")}),process.on("unhandledRejection",async r=>{n.error("Unhandled rejection:",r),await i("unhandledRejection")})}catch(s){n.error("Failed to start MCP Server:",s),process.exit(1)}}function X(l,e=3){let s=l.trim(),t=s.match(/^(\d+)$/);if(t){let r=parseInt(t[1]);if(r>=1&&r<=e)return{action:"select_option",option:t[1]}}let i=s.match(/^(\d+)[,.:;\-\s\n]+(.+)$/s);if(i){let r=parseInt(i[1]);if(r>=1&&r<=e)return{action:"option_with_followup",option:i[1],followUpText:i[2].trim()}}return{action:"send_as_response"}}ae().catch(l=>{n.error("Unhandled error in main:",l),process.exit(1)});0&&(module.exports={parseInteractivePromptInput});
17
+ Please analyze the attached file(s).`,n.info("Prompt updated with attachment paths",{attachmentCount:o.length,newPromptLength:t.length})}}this.trackMobilePrompt(e,t);try{if(await this.promptResponder.answerInteractivePrompt(e,t)){try{await this.appSyncClient.updateEventStatus({eventId:s.eventId,sessionId:s.sessionId,timestamp:s.timestamp,deliveryStatus:a.DeliveryStatus.EXECUTED}),n.info("Event marked as EXECUTED",{eventId:s.eventId})}catch(d){n.warn("Failed to mark event as EXECUTED",{eventId:s.eventId,error:d})}n.info("Mobile prompt sent successfully",{sessionId:e});let c=o.length>0?`Prompt with ${o.length} attachment(s) sent to Claude Code`:`Prompt "${t.substring(0,50)}${t.length>50?"...":""}" sent to Claude Code`;await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:c,metadata:{mobilePrompt:!0,attachmentCount:o.length}})}else n.error("Failed to send mobile prompt",{sessionId:e}),await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:"Failed to send prompt to Claude Code",metadata:{error:!0}})}catch(r){n.error("Failed to execute mobile prompt:",r)}}};async function de(){let l=process.argv[2]||process.env.CLAUDE_SESSION_ID;l?n.info(`Starting MCP server for session: ${l}`):n.info("Starting MCP server without initial session ID (will be set on SessionStart)");let e=new O(l);try{await e.start();let s=e.getPort();console.log(`PORT=${s}`);let t=!1,i=async o=>{if(t){n.info("Shutdown already in progress, ignoring additional signal");return}t=!0,n.info(`Received ${o} signal, stopping server...`);try{await e.stop(),n.info("Graceful shutdown completed"),process.exit(0)}catch(r){n.error("Error during shutdown:",r),process.exit(1)}};process.on("SIGINT",()=>i("SIGINT")),process.on("SIGTERM",()=>i("SIGTERM")),process.on("SIGHUP",()=>i("SIGHUP")),process.on("uncaughtException",async o=>{n.error("Uncaught exception:",o),await i("uncaughtException")}),process.on("unhandledRejection",async o=>{n.error("Unhandled rejection:",o),await i("unhandledRejection")})}catch(s){n.error("Failed to start MCP Server:",s),process.exit(1)}}function z(l,e=3){let s=l.trim(),t=s.match(/^(\d+)$/);if(t){let o=parseInt(t[1]);if(o>=1&&o<=e)return{action:"select_option",option:t[1]}}let i=s.match(/^(\d+)[,.:;\-\s\n]+(.+)$/s);if(i){let o=parseInt(i[1]);if(o>=1&&o<=e)return{action:"option_with_followup",option:i[1],followUpText:i[2].trim()}}return{action:"send_as_response"}}de().catch(l=>{n.error("Unhandled error in main:",l),process.exit(1)});0&&(module.exports={parseInteractivePromptInput});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-claude-plugin",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
4
4
  "description": "Control Claude Code from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
5
5
  "main": "dist/server.js",
6
6
  "bin": {