@quantiya/codevibe-claude-plugin 1.0.48 → 1.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/server.js +6 -6
- package/node_modules/@quantiya/codevibe-core/dist/appsync/appsync-client.d.ts +24 -0
- package/node_modules/@quantiya/codevibe-core/dist/index.js +1 -1
- package/node_modules/@quantiya/codevibe-core/package.json +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codevibe-claude",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.49",
|
|
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")),
|
|
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")),j=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{j.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{j.existsSync(e)&&(j.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="",g="",u=!1,h=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();g+=m,i.debug("Command stderr",{output:m.slice(0,200)})}),a.on("close",d=>{clearTimeout(h);let m={success:d===0&&!u,output:c,error:g,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:g.slice(0,500)}),s(m)}),a.on("error",d=>{clearTimeout(h),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
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
|
|
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 h=this.activeSessions.get(o);if(h?.completedAskUserQuestionFingerprints?.size){let d=h.completedAskUserQuestionFingerprints.size;h.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 h=this.activeSessions.get(o),d;if(h&&e.metadata?.tool_name==="AskUserQuestion"){if(d=this.computeAskUserQuestionFingerprint(e.metadata.tool_input?.questions),d){let Q=h.activeAskUserQuestionFingerprint===d,v=h.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)){h.completedAskUserQuestionFingerprints||(h.completedAskUserQuestionFingerprints=new Set),h.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(h){this.clearPromptState(h),h.waitingForPromptResponse=!0,h.pendingPromptId=e.prompt_id;let m=this.nextPromptGen++;h.promptGenerationToken={promptId:e.prompt_id||"",gen:m},d&&(h.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,g=!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)}),g=!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:g?!0:void 0});if(n===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP){let h=this.activeSessions.get(o);h?.waitingForPromptResponse&&(this.promoteFingerprintAndClearPromptState(h),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(g=>g!==r);if(s.length>0){i.info(`Marking ${s.length} previous session(s) as INACTIVE`);for(let g of s){let u=this.activeSessions.get(g);u?.mobileEndWatcher&&(u.mobileEndWatcher.stop(),u.mobileEndWatcher=void 0),this.appSyncClient.stopHeartbeat(g),this.appSyncClient.cleanupSubscription(g);try{await this.appSyncClient.updateSession({sessionId:g,status:p.SessionStatus.INACTIVE}),i.info("Previous session marked INACTIVE",{prevId:g,newSessionId:r})}catch(h){i.warn("Failed to mark previous session as INACTIVE",{prevId:g,error:h})}u&&this.removePortFile(u.claudeSessionId),this.activeSessions.delete(g)}}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 g=await(0,p.resumeOrCreateSession)({sessionId:r,userId:a.userId,agentType:p.AgentType.CLAUDE,projectPath:n,metadata:e.metadata||{}},this.appSyncClient,i);if(this.sessionKey=g.sessionKey,g.resumed&&!g.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(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,
|
|
5
|
+
`)}}catch(g){if(this.isSessionLimitExceeded(g)){this.displaySubscriptionLimitError(g,"session"),this.activeSessions.delete(r),this.removePortFile(t);return}i.error("Failed to create/resume session:",g)}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 g=process.env[we];if(!g){i.warn("No tmux session set; skipping desktop self-terminate",{sessionId:r,expectedEnv:we});return}await Ue(g,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),this.appSyncClient.cleanupSubscription(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 h={...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,h);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 g=o??n.promptGenerationToken,u=a,h=c;if(!s&&g){let d=g.promptId.length>0?g.promptId:`__prompt_gen_${g.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,h=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=g?.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:g,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=_,H=$,B=!1;F&&(A=p.cryptoService.encryptContent(_,F),H={encrypted:p.cryptoService.encryptMetadata($,F)},B=!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:H,promptId:f,timestamp:(0,p.prepareEventTimestamp)({orderingKey:e}),isEncrypted:B?!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 H=await this.promptResponder.answerInteractivePrompt(e,_,{pressEnter:!1});if(l())return;if(!H){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 B=n.pendingCurrentQuestion?._isSubmit===!0,G=A.toLowerCase(),K;B&&G==="cancel"?K=k?"\u2192 Cancel \u2014 AskUserQuestion cancelled, sending your reply as a new prompt":"\u2192 Cancel \u2014 AskUserQuestion cancelled, no answers submitted":B&&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),h&&n.inFlightEventIds&&n.inFlightEventIds.delete(h)}}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||{}},g=t.metadata?.tool_name,u=t.metadata?.tool_input,h=g==="AskUserQuestion"&&Array.isArray(u?.questions)?u.questions:[];if(h.length>0&&Array.isArray(h[0]?.options)&&h[0].options.length>0){let l=h[0],T=[];if(a)try{let{exec:f}=await import("child_process"),U=_=>new Promise(($,F)=>{f(_,{timeout:5e3},(A,H)=>{A?F(A):$({stdout:H||""})})}),{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(h);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:h.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,g)=>{let u=typeof c=="string",h=u?c:c.label||"",d=u?"":c.description||"",m=u?"":c.preview||"",x={number:String(g+1),text:h};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
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
|
|
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 g=/(?: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+/,h=-1;for(let l=c;l>=0;l-=1)if(u.test(a[l])){h=l;break}let d=h>=0,m=-1;for(let l=c;l>=0;l-=1)if(g.test(a[l])){m=l;break}let x=m<0?1/0:c-m,R=m>=0&&x<=3,Q=m>=0&&h>=0&&m>=h&&m-h<=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:h,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 g=await this.promptResponder.answerInteractivePrompt(e,s,{pressEnter:!0});if(o())return!1;g||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
|
|
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 g="",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&&(g=m)}g||(g={"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}${g}`,d=V.join(c,h);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}
|
|
@@ -304,6 +304,30 @@ export declare class AppSyncClient {
|
|
|
304
304
|
* Send a single heartbeat update.
|
|
305
305
|
*/
|
|
306
306
|
private sendHeartbeat;
|
|
307
|
+
/**
|
|
308
|
+
* Tear down the mobile-event subscription for a SINGLE session.
|
|
309
|
+
*
|
|
310
|
+
* Use this when a session ends or is superseded by a /resume while the
|
|
311
|
+
* daemon keeps running — cleanupSubscriptions() tears down ALL
|
|
312
|
+
* subscriptions and is only appropriate on full daemon shutdown.
|
|
313
|
+
*
|
|
314
|
+
* Without per-session teardown, an ended session's WebSocket survives in
|
|
315
|
+
* the still-running daemon. When AppSync idle-closes it (code 1000) the
|
|
316
|
+
* reconnect path in the start_ack handler re-runs startHeartbeat() and
|
|
317
|
+
* re-asserts status: ACTIVE — resurrecting the ended session as a
|
|
318
|
+
* duplicate row that the mobile app shows as a second live session.
|
|
319
|
+
* cleanupSubscriptionState() sets state.destroyed = true, which is exactly
|
|
320
|
+
* the flag the start_ack and 'close' handlers check to bail out, so this
|
|
321
|
+
* closes the resurrection path.
|
|
322
|
+
*
|
|
323
|
+
* (Confirmed in production logs 2026-06-04: a resumed-over Claude session
|
|
324
|
+
* was marked INACTIVE at 00:23:34 and resurrected to ACTIVE at 03:02:05
|
|
325
|
+
* when its leaked subscription reconnected after an AppSync code-1000
|
|
326
|
+
* close.)
|
|
327
|
+
*
|
|
328
|
+
* Idempotent: a no-op if no subscription is tracked for sessionId.
|
|
329
|
+
*/
|
|
330
|
+
cleanupSubscription(sessionId: string): void;
|
|
307
331
|
/**
|
|
308
332
|
* Cleanup all subscriptions and heartbeats
|
|
309
333
|
*/
|
|
@@ -202,7 +202,7 @@ ${r.stack}`)):typeof r=="object"?o+=` ${JSON.stringify(r,qt)}`:o+=` ${r}`),o}log
|
|
|
202
202
|
updatedAt
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
-
`};var ft=(a=>(a.USER_PROMPT="USER_PROMPT",a.ASSISTANT_RESPONSE="ASSISTANT_RESPONSE",a.TOOL_USE="TOOL_USE",a.NOTIFICATION="NOTIFICATION",a.INTERACTIVE_PROMPT="INTERACTIVE_PROMPT",a.PROMPT_RESPONSE="PROMPT_RESPONSE",a.REASONING="REASONING",a))(ft||{}),Me=(t=>(t.DESKTOP="DESKTOP",t.MOBILE="MOBILE",t))(Me||{}),vt=(r=>(r.SENT="SENT",r.DELIVERED="DELIVERED",r.EXECUTED="EXECUTED",r))(vt||{});var be=(r=>(r.ACTIVE="ACTIVE",r.INACTIVE="INACTIVE",r.PAUSED="PAUSED",r))(be||{}),St=(i=>(i.CLAUDE="CLAUDE",i.GEMINI="GEMINI",i.CODEX="CODEX",i.ANTIGRAVITY="ANTIGRAVITY",i))(St||{});var k={urgentMaxAttempts:10,baseDelayMs:1e3,maxDelayMs:6e4,backoffMultiplier:2,persistentDelayMs:300*1e3},le=class n{constructor(){this.authenticated=!1;this.currentUserId=null;this.currentEmail=null;this.tokens=null;this.activeSubscriptions=new Map;this.pendingRefresh=null;this.lastRefreshFailureAt=null;this.deviceKeyWatcher=null;this.sessionUpdateWatchers=new Map;this.statusWriteChains=new Map;this.heartbeatTimers=new Map;this.environment=A(),c.info("[AppSyncClient] Initialized",{environment:this.environment})}static{this.REFRESH_BACKOFF_MS=3e4}getCurrentUserId(){if(!this.currentUserId)throw new Error("Not authenticated. Call authenticateWithStoredTokens() first.");return this.currentUserId}getCurrentUserEmail(){return this.currentEmail}async authenticateWithStoredTokens(){try{let e=await g.getTokens(this.environment);if(!e)return c.debug("[AppSyncClient] No stored tokens found"),!1;if(c.info("[AppSyncClient] Found stored OAuth tokens",{userId:e.userId,email:e.email,expired:g.isTokenExpired(e)}),g.isTokenExpired(e)){if(c.info("[AppSyncClient] Tokens expired, attempting refresh..."),!await this.refreshTokens(e))return c.warn("[AppSyncClient] Token refresh failed"),!1}else this.tokens=e;return this.currentUserId=this.tokens.userId,this.currentEmail=this.tokens.email,this.authenticated=!0,c.info("[AppSyncClient] Authenticated successfully",{userId:this.currentUserId,email:this.currentEmail}),!0}catch(e){return c.error("[AppSyncClient] Authentication failed:",e),!1}}async refreshTokens(e){if(this.pendingRefresh)return this.pendingRefresh;if(this.lastRefreshFailureAt!==null&&Date.now()-this.lastRefreshFailureAt<n.REFRESH_BACKOFF_MS)return!1;this.pendingRefresh=this.performRefresh(e);try{return await this.pendingRefresh}finally{this.pendingRefresh=null}}async performRefresh(e){let t=await this.callCognitoRefresh(e.refreshToken);if(t!==null)return this.applyRefreshedTokens(e,t);let r=null;try{r=await g.getTokens(this.environment)}catch(i){c.warn("[AppSyncClient] Failed to re-read tokens from storage during refresh recovery",{error:i instanceof Error?i.message:String(i)})}if(r&&r.refreshToken&&r.refreshToken!==e.refreshToken){c.info("[AppSyncClient] In-memory refresh token rejected; retrying with storage-backed token (likely out-of-band re-auth)");let i=await this.callCognitoRefresh(r.refreshToken);if(i!==null)return this.applyRefreshedTokens(r,i)}return this.lastRefreshFailureAt=Date.now(),!1}async callCognitoRefresh(e){try{let t=w(),r=`https://${t.aws.cognitoDomain}/oauth2/token`,i=new URLSearchParams({grant_type:"refresh_token",client_id:t.aws.cognitoClientId,refresh_token:e}),s=await te(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:i.toString()},"Token refresh");return s.ok?await s.json():(c.error("[AppSyncClient] Token refresh failed",{status:s.status}),null)}catch(t){return c.error("[AppSyncClient] Token refresh error:",t),null}}async applyRefreshedTokens(e,t){let r={...e,accessToken:t.access_token,idToken:t.id_token,expiresAt:Date.now()+t.expires_in*1e3};this.tokens=r,this.lastRefreshFailureAt=null;try{await g.setTokens(r,this.environment),c.info("[AppSyncClient] Tokens refreshed",{expiresAt:new Date(r.expiresAt).toISOString()})}catch(i){c.warn("[AppSyncClient] Tokens refreshed but persistence failed; daemon keeps using fresh tokens in memory. A restart while persistence is still broken would lose them.",{error:i instanceof Error?i.message:String(i),expiresAt:new Date(r.expiresAt).toISOString()})}return!0}isAuthenticated(){return this.authenticated}signOut(){this.authenticated=!1,this.tokens=null,this.currentUserId=null,this.currentEmail=null,this.cleanupSubscriptions(),c.info("[AppSyncClient] Signed out")}async graphqlRequest(e,t,r=!1){let i=w();if(!this.tokens?.idToken)throw new Error('Not authenticated. Run "codevibe login" first.');let s={"Content-Type":"application/json",Authorization:this.tokens.idToken},o=await te(i.aws.appsyncUrl,{method:"POST",headers:s,body:JSON.stringify({query:e,variables:t})},"AppSync GraphQL request"),a=await o.json();if(o.status===401&&!r&&this.tokens){if(c.info("[AppSyncClient] 401 Unauthorized, refreshing token..."),await this.refreshTokens(this.tokens))return this.graphqlRequest(e,t,!0);throw new Error("Token expired and refresh failed")}if(!o.ok)throw new Error(`GraphQL request failed: ${o.status}`);if(a.errors?.length)throw new Error(`GraphQL error: ${a.errors[0].message}`);return a}async createSession(e){let t={...e,metadata:e.metadata?JSON.stringify(e.metadata):void 0},r=await this.graphqlRequest(x.createSession,{input:t});return c.info("[AppSyncClient] Session created",{sessionId:r.data.createSession.sessionId}),r.data.createSession}async updateSession(e){if(e.status===void 0)return this.doUpdateSession(e);let r=(this.statusWriteChains.get(e.sessionId)??Promise.resolve()).catch(()=>{}).then(()=>this.doUpdateSession(e));return this.statusWriteChains.set(e.sessionId,r.catch(()=>{})),r}async doUpdateSession(e){let t={...e,metadata:e.metadata?JSON.stringify(e.metadata):void 0},r=await this.graphqlRequest(x.updateSession,{input:t});return c.debug("[AppSyncClient] Session updated",{sessionId:r.data.updateSession.sessionId}),r.data.updateSession}async getSession(e){return(await this.graphqlRequest(U.getSession,{sessionId:e})).data.getSession}async createEvent(e){let t={...e,metadata:e.metadata?JSON.stringify(e.metadata):void 0},r=await this.graphqlRequest(x.createEvent,{input:t});return c.debug("[AppSyncClient] Event created",{eventId:r.data.createEvent.eventId,type:r.data.createEvent.type}),r.data.createEvent}async updateEventStatus(e){return(await this.graphqlRequest(x.updateEventStatus,{input:e})).data.updateEventStatus}async listEvents(e,t,r){return(await this.graphqlRequest(U.listEvents,{sessionId:e,source:t,limit:r})).data.listEvents.items}async listSessions(e=100){if(!this.currentUserId)throw new Error("Not authenticated");let t=[],r=null;do{let s=(await this.graphqlRequest(U.listSessions,{userId:this.currentUserId,limit:e,nextToken:r})).data?.listSessions;s?.items&&t.push(...s.items),r=s?.nextToken??null}while(r);return t}async sweepOrphanSessions(e){let t=e.staleThresholdMs??9e5,r=new Set(e.excludeSessionIds??[]),i=Date.now(),s;try{s=await this.listSessions()}catch(a){return c.warn("[AppSyncClient] OrphanSweep: listSessions failed, skipping sweep",{agentType:e.agentType,error:a instanceof Error?a.message:String(a)}),0}let o=0;for(let a of s){if(a.agentType!==e.agentType||a.status!=="ACTIVE"||r.has(a.sessionId)||!a.lastHeartbeatAt)continue;let d=i-new Date(a.lastHeartbeatAt).getTime();if(!(d<t)){c.warn("[AppSyncClient] OrphanSweep: marking stale session INACTIVE",{sessionId:a.sessionId,agentType:a.agentType,lastHeartbeatAt:a.lastHeartbeatAt,heartbeatAgeMinutes:Math.round(d/6e4)});try{await this.updateSession({sessionId:a.sessionId,status:"INACTIVE"}),o++}catch(l){c.warn("[AppSyncClient] OrphanSweep: updateSession failed, leaving row as-is",{sessionId:a.sessionId,error:l instanceof Error?l.message:String(l)})}}}return o>0&&c.info("[AppSyncClient] OrphanSweep complete",{agentType:e.agentType,swept:o}),o}async listUserDeviceKeys(){return(await this.graphqlRequest(U.listUserDeviceKeys,{})).data.listUserDeviceKeys||[]}async registerDeviceKey(e,t,r,i){let s={deviceId:e,publicKey:t,platform:r,deviceName:i};await this.graphqlRequest(x.registerDeviceKey,{input:s}),c.info("[AppSyncClient] Device key registered",{deviceId:e,platform:r})}async grantSessionKey(e){await this.graphqlRequest(x.grantSessionKey,{input:e}),c.info("[AppSyncClient] Session key granted",{sessionId:e.sessionId,deviceId:e.deviceId})}async getAttachmentDownloadUrl(e){return(await this.graphqlRequest(x.getAttachmentDownloadUrl,{s3Key:e})).data.getAttachmentDownloadUrl}subscribeToEvents(e,t,r){c.info("[AppSyncClient] Subscribing to events",{sessionId:e});let i=this.activeSubscriptions.get(e);i&&(this.cleanupSubscriptionState(i),this.activeSubscriptions.delete(e));let s={ws:null,subscriptionId:(0,z.v4)(),sessionId:e,onEvent:t,onError:r,reconnectAttempts:0,isReconnecting:!1,destroyed:!1};return this.activeSubscriptions.set(e,s),this.createSubscription(s),()=>{this.cleanupSubscriptionState(s),this.activeSubscriptions.delete(e)}}buildRealtimeUrl(){let e=w(),t=new URL(e.aws.appsyncUrl),i=/\.appsync-api\.[^.]+\.amazonaws\.com$/.test(t.host)?e.aws.appsyncUrl.replace("https://","wss://").replace("appsync-api","appsync-realtime-api"):`wss://${t.host}/graphql/realtime`,s={host:t.host};this.tokens?.idToken&&(s.Authorization=this.tokens.idToken);let o=Buffer.from(JSON.stringify(s)).toString("base64"),a=Buffer.from(JSON.stringify({})).toString("base64");return`${i}?header=${o}&payload=${a}`}createSubscription(e){let{sessionId:t,subscriptionId:r,onEvent:i,onError:s}=e;try{let o=this.buildRealtimeUrl(),a=new j.default(o,["graphql-ws"]);a.on("open",()=>{c.info("[AppSyncClient] WebSocket connected",{sessionId:t}),a.send(JSON.stringify({type:"connection_init"}))}),a.on("message",d=>{try{let l=JSON.parse(d.toString());switch(l.type){case"connection_ack":this.sendSubscriptionStart(a,e);break;case"start_ack":if(e.destroyed)break;c.info("[AppSyncClient] Subscription started",{sessionId:t});let y=e.reconnectAttempts>0;e.isReconnecting=!1,e.reconnectAttempts=0,this.startHeartbeat(t),y&&this.updateSession({sessionId:t,status:"ACTIVE"}).then(()=>c.info("[AppSyncClient] Re-asserted session ACTIVE after reconnect",{sessionId:t})).catch(m=>c.warn("[AppSyncClient] Re-assert ACTIVE after reconnect failed",{sessionId:t,error:m instanceof Error?m.message:String(m)}));break;case"data":this.resetKeepAliveTimer(e);let p=l.payload?.data?.onEventCreated;p&&p.source==="MOBILE"&&i(p);break;case"ka":this.resetKeepAliveTimer(e);break;case"error":let h=l.payload?.errors?.[0]?.message||"Unknown error";this.handleSubscriptionError(e,new Error(h));break}}catch(l){c.error("[AppSyncClient] Failed to parse message",{error:l})}}),a.on("error",d=>{c.error("[AppSyncClient] WebSocket error",{sessionId:t,error:d.message}),this.handleSubscriptionError(e,d)}),a.on("close",(d,l)=>{c.info("[AppSyncClient] WebSocket closed",{sessionId:t,code:d}),e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),!e.destroyed&&this.activeSubscriptions.get(t)===e&&this.handleSubscriptionError(e,new Error(`WebSocket closed: ${d}`))}),e.ws=a,this.resetKeepAliveTimer(e)}catch(o){this.handleSubscriptionError(e,o)}}sendSubscriptionStart(e,t){let r=w(),{sessionId:i,subscriptionId:s}=t,o={host:new URL(r.aws.appsyncUrl).host};this.tokens?.idToken&&(o.Authorization=this.tokens.idToken),e.send(JSON.stringify({id:s,type:"start",payload:{data:JSON.stringify({query:J.onEventCreated,variables:{sessionId:i}}),extensions:{authorization:o}}}))}resetKeepAliveTimer(e){e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),e.keepAliveTimer=setTimeout(()=>{this.handleSubscriptionError(e,new Error("Keep-alive timeout"))},300*1e3)}handleSubscriptionError(e,t){let{sessionId:r,onError:i}=e;if(e.isReconnecting||!this.activeSubscriptions.has(r))return;e.isReconnecting=!0,e.reconnectAttempts++,this.stopHeartbeat(r);let s=e.reconnectAttempts<=k.urgentMaxAttempts,o;if(s?o=Math.min(k.baseDelayMs*Math.pow(k.backoffMultiplier,e.reconnectAttempts-1),k.maxDelayMs):(o=k.persistentDelayMs,e.reconnectAttempts===k.urgentMaxAttempts+1&&c.info("[AppSyncClient] Switching to persistent reconnect (every 5min)",{sessionId:r})),c.info("[AppSyncClient] Scheduling reconnect",{sessionId:r,attempt:e.reconnectAttempts,phase:s?"urgent":"persistent",delayMs:o}),e.ws){try{e.ws.close(1e3)}catch{}e.ws=null}e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),e.reconnectTimer=setTimeout(async()=>{if(e.isReconnecting=!1,e.destroyed||this.activeSubscriptions.get(r)!==e){c.info("[AppSyncClient] Reconnect skipped \u2014 state is no longer canonical",{sessionId:r});return}try{let a=await g.getTokens(this.environment);a&&(g.isTokenExpired(a)?await this.refreshTokens(a)&&c.info("[AppSyncClient] Tokens refreshed before reconnect",{sessionId:r}):this.tokens=a)}catch{c.warn("[AppSyncClient] Token refresh failed before reconnect, using existing tokens",{sessionId:r})}if(e.destroyed||this.activeSubscriptions.get(r)!==e){c.info("[AppSyncClient] Reconnect skipped after token refresh \u2014 state no longer canonical",{sessionId:r});return}e.subscriptionId=(0,z.v4)(),this.createSubscription(e)},o)}cleanupSubscriptionState(e){if(e.destroyed=!0,e.reconnectTimer&&(clearTimeout(e.reconnectTimer),e.reconnectTimer=void 0),e.keepAliveTimer&&(clearTimeout(e.keepAliveTimer),e.keepAliveTimer=void 0),e.ws){try{e.ws.readyState===j.default.OPEN&&e.ws.send(JSON.stringify({id:e.subscriptionId,type:"stop"}))}catch{}try{e.ws.close(1e3)}catch{}try{e.ws.removeAllListeners()}catch{}e.ws=null}}subscribeToDeviceKeyRegistered(e,t,r,i){c.info("[AppSyncClient] Subscribing to device key registrations",{userId:e}),this.deviceKeyWatcher&&this.stopDeviceKeyWatcherInternal();let s={userId:e,subscriptionId:(0,z.v4)(),ws:null,onNewDevice:t,onReconnect:r,onError:i,reconnectAttempts:0,isReconnecting:!1,destroyed:!1};return this.deviceKeyWatcher=s,this.createDeviceKeyWatcherConnection(s),()=>{this.stopDeviceKeyWatcherInternal()}}stopDeviceKeyWatcher(){this.stopDeviceKeyWatcherInternal()}stopDeviceKeyWatcherInternal(){let e=this.deviceKeyWatcher;if(e){if(e.destroyed=!0,e.reconnectTimer&&(clearTimeout(e.reconnectTimer),e.reconnectTimer=void 0),e.keepAliveTimer&&(clearTimeout(e.keepAliveTimer),e.keepAliveTimer=void 0),e.ws){try{e.ws.readyState===j.default.OPEN&&e.ws.send(JSON.stringify({id:e.subscriptionId,type:"stop"}))}catch{}try{e.ws.close(1e3)}catch{}try{e.ws.removeAllListeners()}catch{}e.ws=null}this.deviceKeyWatcher=null,c.info("[AppSyncClient] Device key watcher stopped")}}createDeviceKeyWatcherConnection(e){try{let t=this.buildRealtimeUrl(),r=new j.default(t,["graphql-ws"]);r.on("open",()=>{c.info("[AppSyncClient] Device key watcher WebSocket connected",{userId:e.userId}),r.send(JSON.stringify({type:"connection_init"}))}),r.on("message",i=>{try{let s=JSON.parse(i.toString());switch(s.type){case"connection_ack":this.sendDeviceKeyWatcherStart(r,e);break;case"start_ack":c.info("[AppSyncClient] Device key watcher subscription started",{userId:e.userId});let o=e.isReconnecting;if(e.isReconnecting=!1,e.reconnectAttempts=0,o&&e.onReconnect)try{e.onReconnect()}catch(l){c.warn("[AppSyncClient] Device key watcher onReconnect handler threw",{error:l})}break;case"data":this.resetDeviceKeyWatcherKeepAlive(e);let a=s.payload?.data?.onDeviceKeyRegistered;if(a){c.info("[AppSyncClient] Device key registration observed",{userId:e.userId,newDeviceId:a.deviceId,platform:a.platform});try{e.onNewDevice(a)}catch(l){c.warn("[AppSyncClient] Device key watcher onNewDevice handler threw",{error:l})}}break;case"ka":this.resetDeviceKeyWatcherKeepAlive(e);break;case"error":let d=s.payload?.errors?.[0]?.message||"Unknown error";this.handleDeviceKeyWatcherError(e,new Error(d));break}}catch(s){c.error("[AppSyncClient] Failed to parse device key watcher message",{error:s})}}),r.on("error",i=>{c.error("[AppSyncClient] Device key watcher WebSocket error",{userId:e.userId,error:i.message}),this.handleDeviceKeyWatcherError(e,i)}),r.on("close",i=>{c.info("[AppSyncClient] Device key watcher WebSocket closed",{userId:e.userId,code:i}),e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),!e.destroyed&&this.deviceKeyWatcher===e&&this.handleDeviceKeyWatcherError(e,new Error(`WebSocket closed: ${i}`))}),e.ws=r,this.resetDeviceKeyWatcherKeepAlive(e)}catch(t){this.handleDeviceKeyWatcherError(e,t)}}sendDeviceKeyWatcherStart(e,t){let r=w(),{userId:i,subscriptionId:s}=t,o={host:new URL(r.aws.appsyncUrl).host};this.tokens?.idToken&&(o.Authorization=this.tokens.idToken),e.send(JSON.stringify({id:s,type:"start",payload:{data:JSON.stringify({query:J.onDeviceKeyRegistered,variables:{userId:i}}),extensions:{authorization:o}}}))}resetDeviceKeyWatcherKeepAlive(e){e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),e.keepAliveTimer=setTimeout(()=>{this.handleDeviceKeyWatcherError(e,new Error("Device key watcher keep-alive timeout"))},300*1e3)}handleDeviceKeyWatcherError(e,t){if(e.isReconnecting||e.destroyed||this.deviceKeyWatcher!==e)return;if(e.isReconnecting=!0,e.reconnectAttempts++,e.onError)try{e.onError(t)}catch{}if(e.ws){try{e.ws.removeAllListeners()}catch{}try{e.ws.close(1e3)}catch{}e.ws=null}e.keepAliveTimer&&(clearTimeout(e.keepAliveTimer),e.keepAliveTimer=void 0);let i=e.reconnectAttempts<=k.urgentMaxAttempts?Math.min(k.baseDelayMs*Math.pow(k.backoffMultiplier,e.reconnectAttempts-1),k.maxDelayMs):k.persistentDelayMs;c.warn("[AppSyncClient] Device key watcher reconnect scheduled",{userId:e.userId,attempts:e.reconnectAttempts,delayMs:i,error:t.message}),e.reconnectTimer=setTimeout(async()=>{if(e.isReconnecting=!1,e.destroyed||this.deviceKeyWatcher!==e){c.info("[AppSyncClient] Device key watcher reconnect skipped \u2014 state no longer canonical",{userId:e.userId});return}try{let s=await g.getTokens(this.environment);s&&(g.isTokenExpired(s)?await this.refreshTokens(s)&&c.info("[AppSyncClient] Tokens refreshed before device key watcher reconnect",{userId:e.userId}):this.tokens=s)}catch{c.warn("[AppSyncClient] Token refresh failed before device key watcher reconnect, using existing tokens",{userId:e.userId})}e.destroyed||this.deviceKeyWatcher!==e||(e.subscriptionId=(0,z.v4)(),this.createDeviceKeyWatcherConnection(e))},i)}watchForMobileEnd(e,t){c.info("[AppSyncClient] Starting mobile-end watcher",{sessionId:e});let r=this.sessionUpdateWatchers.get(e);r&&(c.info("[AppSyncClient] Replacing existing mobile-end watcher",{sessionId:e}),this.cleanupSessionUpdateWatcherState(r),this.sessionUpdateWatchers.delete(e));let i={sessionId:e,subscriptionId:(0,z.v4)(),ws:null,onMobileEndRequested:t,priorStatus:"ACTIVE",firedOnce:!1,reconnectAttempts:0,isReconnecting:!1,destroyed:!1};return this.sessionUpdateWatchers.set(e,i),this.createSessionUpdateWatcherConnection(i),{stop:()=>{this.sessionUpdateWatchers.get(e)===i&&(this.cleanupSessionUpdateWatcherState(i),this.sessionUpdateWatchers.delete(e),c.info("[AppSyncClient] Mobile-end watcher stopped",{sessionId:e}))}}}createSessionUpdateWatcherConnection(e){try{let t=this.buildRealtimeUrl(),r=new j.default(t,["graphql-ws"]);r.on("open",()=>{c.info("[AppSyncClient] Mobile-end watcher WebSocket connected",{sessionId:e.sessionId}),r.send(JSON.stringify({type:"connection_init"}))}),r.on("message",i=>{try{let s=JSON.parse(i.toString());switch(s.type){case"connection_ack":this.sendSessionUpdateWatcherStart(r,e);break;case"start_ack":c.info("[AppSyncClient] Mobile-end watcher subscription started",{sessionId:e.sessionId}),e.isReconnecting=!1,e.reconnectAttempts=0;break;case"data":this.resetSessionUpdateWatcherKeepAlive(e),this.handleSessionUpdatePayload(e,s.payload);break;case"ka":this.resetSessionUpdateWatcherKeepAlive(e);break;case"error":let o=s.payload?.errors?.[0]?.message||"Unknown error";this.handleSessionUpdateWatcherError(e,new Error(o));break}}catch(s){c.error("[AppSyncClient] Failed to parse mobile-end watcher message",{error:s})}}),r.on("error",i=>{c.error("[AppSyncClient] Mobile-end watcher WebSocket error",{sessionId:e.sessionId,error:i.message}),this.handleSessionUpdateWatcherError(e,i)}),r.on("close",i=>{c.info("[AppSyncClient] Mobile-end watcher WebSocket closed",{sessionId:e.sessionId,code:i}),e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),!e.destroyed&&this.sessionUpdateWatchers.get(e.sessionId)===e&&this.handleSessionUpdateWatcherError(e,new Error(`WebSocket closed: ${i}`))}),e.ws=r,this.resetSessionUpdateWatcherKeepAlive(e)}catch(t){this.handleSessionUpdateWatcherError(e,t)}}handleSessionUpdatePayload(e,t){let r=t?.data?.onSessionUpdated;if(!r){c.warn("[AppSyncClient] Mobile-end watcher received malformed payload",{sessionId:e.sessionId});return}if(e.firedOnce)return;let i=r.status;if(i==null){c.debug("[AppSyncClient] Mobile-end watcher skipped non-status payload",{sessionId:e.sessionId});return}if(e.priorStatus==="ACTIVE"&&i==="INACTIVE"){e.firedOnce=!0,e.priorStatus="INACTIVE",c.info("[AppSyncClient] Mobile end requested for session",{sessionId:e.sessionId}),Promise.resolve().then(()=>e.onMobileEndRequested()).catch(s=>{c.warn("[AppSyncClient] Mobile-end callback threw",{sessionId:e.sessionId,error:s})});return}e.priorStatus=i}sendSessionUpdateWatcherStart(e,t){let r=w(),{sessionId:i,subscriptionId:s}=t,o={host:new URL(r.aws.appsyncUrl).host};this.tokens?.idToken&&(o.Authorization=this.tokens.idToken),e.send(JSON.stringify({id:s,type:"start",payload:{data:JSON.stringify({query:J.onSessionUpdated,variables:{sessionId:i}}),extensions:{authorization:o}}}))}resetSessionUpdateWatcherKeepAlive(e){e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),e.keepAliveTimer=setTimeout(()=>{this.handleSessionUpdateWatcherError(e,new Error("Mobile-end watcher keep-alive timeout"))},300*1e3)}handleSessionUpdateWatcherError(e,t){if(e.isReconnecting||e.destroyed||this.sessionUpdateWatchers.get(e.sessionId)!==e)return;if(e.isReconnecting=!0,e.reconnectAttempts++,e.ws){try{e.ws.removeAllListeners()}catch{}try{e.ws.close(1e3)}catch{}e.ws=null}e.keepAliveTimer&&(clearTimeout(e.keepAliveTimer),e.keepAliveTimer=void 0);let i=e.reconnectAttempts<=k.urgentMaxAttempts?Math.min(k.baseDelayMs*Math.pow(k.backoffMultiplier,e.reconnectAttempts-1),k.maxDelayMs):k.persistentDelayMs;c.warn("[AppSyncClient] Mobile-end watcher reconnect scheduled",{sessionId:e.sessionId,attempts:e.reconnectAttempts,delayMs:i,error:t.message}),e.reconnectTimer=setTimeout(async()=>{if(e.isReconnecting=!1,!(e.destroyed||this.sessionUpdateWatchers.get(e.sessionId)!==e)){try{let s=await g.getTokens(this.environment);s&&(g.isTokenExpired(s)?await this.refreshTokens(s):this.tokens=s)}catch{c.warn("[AppSyncClient] Token refresh failed before mobile-end watcher reconnect",{sessionId:e.sessionId})}e.destroyed||this.sessionUpdateWatchers.get(e.sessionId)!==e||(e.subscriptionId=(0,z.v4)(),this.createSessionUpdateWatcherConnection(e))}},i)}cleanupSessionUpdateWatcherState(e){if(e.destroyed=!0,e.reconnectTimer&&(clearTimeout(e.reconnectTimer),e.reconnectTimer=void 0),e.keepAliveTimer&&(clearTimeout(e.keepAliveTimer),e.keepAliveTimer=void 0),e.ws){try{e.ws.readyState===j.default.OPEN&&e.ws.send(JSON.stringify({id:e.subscriptionId,type:"stop"}))}catch{}try{e.ws.close(1e3)}catch{}try{e.ws.removeAllListeners()}catch{}e.ws=null}}startHeartbeat(e,t=120*1e3){this.stopHeartbeat(e),this.sendHeartbeat(e);let r=setInterval(()=>{this.sendHeartbeat(e)},t);this.heartbeatTimers.set(e,r),c.info("[AppSyncClient] Heartbeat started",{sessionId:e,intervalMs:t})}stopHeartbeat(e){let t=this.heartbeatTimers.get(e);t&&(clearInterval(t),this.heartbeatTimers.delete(e),c.info("[AppSyncClient] Heartbeat stopped",{sessionId:e}))}async sendHeartbeat(e){try{await this.updateSession({sessionId:e,lastHeartbeatAt:new Date().toISOString()}),c.debug("[AppSyncClient] Heartbeat sent",{sessionId:e})}catch(t){c.warn("[AppSyncClient] Heartbeat failed",{sessionId:e,error:t})}}cleanupSubscriptions(){this.activeSubscriptions.forEach(e=>{this.cleanupSubscriptionState(e)}),this.activeSubscriptions.clear(),this.stopDeviceKeyWatcherInternal(),this.sessionUpdateWatchers.forEach(e=>{this.cleanupSessionUpdateWatcherState(e)}),this.sessionUpdateWatchers.clear(),this.heartbeatTimers.forEach(e=>clearInterval(e)),this.heartbeatTimers.clear()}};var Dt=v(require("crypto")),_t=v(require("fs")),Rt=v(require("http")),Kt=require("child_process");ee();P();H();var We=v(require("crypto")),wt=v(require("https")),kt=v(require("os")),er="G-GS74YEQTB8",tr="lAfOF6OxRzSQ-NsLBRjhAg",rr="www.google-analytics.com",nr=`/mp/collect?measurement_id=${er}&api_secret=${tr}`,ir={port_in_use:"server_start",port_range_exhausted:"server_start",server_listen_failed:"server_start",browser_open_failed:"browser_open",login_timeout:"awaiting_callback",cognito_rejected:"awaiting_callback",state_mismatch:"awaiting_callback",no_authorization_code:"awaiting_callback",token_exchange_failed:"exchanging_code",token_exchange_network_error:"exchanging_code",keychain_write_failed:"storing_tokens",user_aborted:"unknown",unknown:"unknown"};function sr(){let n=typeof process.getuid=="function"?process.getuid():0;return We.createHash("sha256").update(`${kt.hostname()}-${n}`).digest("hex").substring(0,36)}function $(){return{platform:process.platform,source:process.env.CODEVIBE_TELEMETRY_SOURCE||"production"}}async function L(n,e){try{let t=JSON.stringify({client_id:sr(),events:[{name:n,params:e}]});await new Promise(r=>{let i=wt.request({hostname:rr,path:nr,method:"POST",headers:{"Content-Type":"application/json"}},()=>r());i.on("error",()=>r()),i.write(t),i.end(),setTimeout(r,2e3)})}catch{}}async function pe(n){await L("auth_completed",{...$(),user_id:n})}async function I(n,e){let t={...$(),reason:n,stage:e?.stage??ir[n]};if(typeof e?.httpStatus=="number"&&(t.http_status=e.httpStatus),e?.errorFragment){let{homedir:r}=await import("os"),i=e.errorFragment.replace(/\x1b\[[0-9;]*[a-zA-Z]/g,"").replace(/\\/g,"/").replace(/[\n\r\t"]/g," ").replace(/[^\x20-\x7E]/g,"").trim(),s=[process.env.HOME,process.env.USERPROFILE,(()=>{try{return r()}catch{return}})()].filter(d=>typeof d=="string"&&d.length>0).map(d=>d.replace(/\\/g,"/"));for(let d of s){let l=d.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");i=i.replace(new RegExp(l,"g"),"~")}i=i.replace(/\/Users\/[^/ ]+/g,"/Users/<user>").replace(/\/home\/[^/ ]+/g,"/home/<user>").replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,"<email>");let o=i.substring(0,100),a=i.substring(100,200);o&&(t.error_fragment=o),a&&(t.error_fragment_2=a)}await L("auth_failed",t)}var Be=Symbol.for("codevibe.auth.beaconed"),bt=Symbol.for("codevibe.auth.failureReason");function C(n,e){try{Object.defineProperty(n,Be,{value:!0,enumerable:!1,configurable:!0,writable:!1}),Object.defineProperty(n,bt,{value:e,enumerable:!1,configurable:!0,writable:!1})}catch{}return n}function ue(n){return!!(n&&typeof n=="object"&&n[Be])}function Fe(n){if(n&&typeof n=="object"&&n[Be]){let e=n[bt];if(typeof e=="string")return e}}function re(n){return n<=0?"0":n===1?"1":n<=5?"2-5":"6+"}function Ee(n){return We.createHash("sha256").update(n).digest("hex").slice(0,8)}async function Et(n){return L("session_encryption_device_skipped",{...$(),...n})}async function It(n){return L("session_encryption_partial_success",{...$(),...n})}async function At(n){return L("session_encryption_catch_up_grant",{...$(),...n})}async function Tt(n){return L("session_encryption_self_rekey_request",{...$(),...n})}async function Ct(n){return L("session_encryption_self_rekey_success",{...$(),...n})}async function xt(n){return L("session_encryption_self_rekey_timeout",{...$(),...n})}var ne=8080,ie=20,He="/callback";async function he(n){let e=null;for(let t=0;t<ie;t++){let r=ne+t;try{let i=await new Promise((s,o)=>{let a=Rt.createServer(n),d=y=>{a.removeListener("listening",l),o(y)},l=()=>{a.removeListener("error",d),a.on("error",y=>{c.error("[AuthService] OAuth server post-bind error",{port:r,code:y?.code,message:y?.message})}),s(a)};a.once("error",d),a.once("listening",l),a.listen(r,"localhost")});return c.info(`[AuthService] OAuth server bound on port ${r} (attempt ${t+1}/${ie})`),{server:i,port:r}}catch(i){if(e=i,i?.code==="EADDRINUSE")continue;throw i}}throw Object.assign(new Error(`All ports ${ne}-${ne+ie-1} are in use. Free at least one for OAuth callback or quit a conflicting service (common collisions: Vite, Webpack, Spring Boot, Docker exposed ports). Underlying: ${e?.message??"EADDRINUSE"}`),{code:"EADDRINUSE_ALL"})}var se=class n{constructor(){}static getInstance(){return n.instance||(n.instance=new n),n.instance}openBrowser(e){console.error(""),console.error("Opening your browser for sign-in..."),this.isRunningInWSL()?console.error("If your browser does not open, paste this URL in your Windows browser:"):console.error("If your browser does not open automatically, visit this URL:"),console.error(` ${e}`),console.error("");let t=this.getBrowserCommands();this.tryBrowserCommand(t,e,0)}getBrowserCommands(){let e=process.platform;if(e==="darwin")return[{cmd:"open",fixedArgs:[]}];if(e==="win32")return[{cmd:"cmd",fixedArgs:["/c","start",""]}];let t=[];return this.isRunningInWSL()&&(t.push({cmd:"wslview",fixedArgs:[]}),t.push({cmd:"cmd.exe",fixedArgs:["/c","start",""]}),t.push({cmd:"powershell.exe",fixedArgs:["-NoProfile","-Command","Start-Process"]})),t.push({cmd:"xdg-open",fixedArgs:[]}),t}isRunningInWSL(){if(process.platform!=="linux")return!1;try{let e=_t.readFileSync("/proc/sys/kernel/osrelease","utf8");return/microsoft|wsl/i.test(e)}catch{return!1}}tryBrowserCommand(e,t,r){if(r>=e.length){c.debug("[AuthService] No browser-opening command succeeded. User must open the sign-in URL manually (printed to stderr above)."),console.error(""),console.error("\u26A0\uFE0F Could not open browser automatically."),this.isRunningInWSL()?console.error(" WSL detected \u2014 paste this URL in your Windows browser:"):console.error(" Please copy and paste this URL into your browser:"),console.error(` ${t}`),console.error("");return}let i=e[r],s=[...i.fixedArgs,t],o=!1,a=p=>{o||(o=!0,c.debug(`[AuthService] Browser command '${i.cmd}' ${p}; trying next fallback`),this.tryBrowserCommand(e,t,r+1))},d=p=>{o||(o=!0,c.debug(`[AuthService] Browser command '${i.cmd}' ${p}`))},l;try{l=(0,Kt.spawn)(i.cmd,s,{detached:!0,stdio:"ignore"})}catch(p){a(`threw synchronously: ${p?.message||p}`);return}l.on("error",p=>{a(`failed to spawn: ${p?.message||p}`)}),l.on("exit",(p,h)=>{p===0?d("exited successfully"):a(h?`terminated by signal ${h}`:`exited with code ${p}`)}),setTimeout(()=>{d("still running after 3s, assuming success")},3e3).unref(),l.unref()}generateState(){return Dt.randomBytes(32).toString("hex")}buildAuthUrl(e,t){let r=w(),i=new URLSearchParams({client_id:r.aws.cognitoClientId,response_type:"code",scope:"email openid profile",redirect_uri:t,state:e});return`https://${r.aws.cognitoDomain}/oauth2/authorize?${i.toString()}`}async exchangeCodeForTokens(e,t){let r=w(),i=`https://${r.aws.cognitoDomain}/oauth2/token`,s=new URLSearchParams({grant_type:"authorization_code",client_id:r.aws.cognitoClientId,code:e,redirect_uri:t}),o;try{o=await te(i,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:s.toString()},"Token exchange")}catch(d){throw await I("token_exchange_network_error"),C(d,"token_exchange_network_error"),d}if(!o.ok){let d=await o.text(),l=new Error(`Token exchange failed: ${o.status} ${d}`);throw await I("token_exchange_failed",{httpStatus:o.status}),C(l,"token_exchange_failed"),l}let a=await o.json();return{accessToken:a.access_token,idToken:a.id_token,refreshToken:a.refresh_token,expiresIn:a.expires_in}}decodeJwt(e){let t=e.split(".");if(t.length!==3)throw new Error("Invalid JWT");return JSON.parse(Buffer.from(t[1],"base64").toString("utf-8"))}async refreshTokens(e){let t=w(),r=`https://${t.aws.cognitoDomain}/oauth2/token`,i=new URLSearchParams({grant_type:"refresh_token",client_id:t.aws.cognitoClientId,refresh_token:e}),s=await te(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:i.toString()},"Token refresh");if(!s.ok)throw new Error(`Token refresh failed: ${s.status}`);let o=await s.json();return{accessToken:o.access_token,idToken:o.id_token,expiresIn:o.expires_in}}async login(){let e=await g.getTokens(A());if(e&&!g.isTokenExpired(e))return e;let t=this.generateState();return new Promise((r,i)=>{let s={},o=null,a=!1,d=!1,l=h=>{h.closeAllConnections?.()},y=h=>{if(a)return;a=!0,o&&(clearTimeout(o),o=null);let m=s.server;m?(l(m),m.close(()=>r(h))):r(h)},p=h=>{if(a)return;a=!0,o&&(clearTimeout(o),o=null);let m=s.server;m?(l(m),m.close(()=>i(h))):i(h)};(async()=>{let h;try{h=await he(async(f,S)=>{if(d||a){S.writeHead(200,{Connection:"close"}),S.end();return}let Ze=`http://localhost:${f.socket?.localPort??h.port}${He}`,ye=new URL(f.url||"",Ze);if(ye.pathname!==He){S.writeHead(404,{Connection:"close"}),S.end("Not found");return}try{let B=ye.searchParams.get("code"),Te=ye.searchParams.get("state"),X=ye.searchParams.get("error");if(X){let b=new Error(`OAuth error: ${X}`);throw await I("cognito_rejected"),C(b,"cognito_rejected"),b}if(Te!==t){let b=new Error("State mismatch");throw await I("state_mismatch"),C(b,"state_mismatch"),b}if(!B){let b=new Error("No authorization code");throw await I("no_authorization_code"),C(b,"no_authorization_code"),b}d=!0;let oe=await this.exchangeCodeForTokens(B,Ze),Qe=this.decodeJwt(oe.idToken),Ce={accessToken:oe.accessToken,idToken:oe.idToken,refreshToken:oe.refreshToken,expiresAt:Date.now()+oe.expiresIn*1e3,userId:Qe.sub,email:Qe.email||"unknown"};try{await g.setTokens(Ce,A())}catch(b){throw await I("keychain_write_failed"),C(b,"keychain_write_failed"),b}S.writeHead(200,{"Content-Type":"text/html; charset=utf-8",Connection:"close"}),S.end(`
|
|
205
|
+
`};var ft=(a=>(a.USER_PROMPT="USER_PROMPT",a.ASSISTANT_RESPONSE="ASSISTANT_RESPONSE",a.TOOL_USE="TOOL_USE",a.NOTIFICATION="NOTIFICATION",a.INTERACTIVE_PROMPT="INTERACTIVE_PROMPT",a.PROMPT_RESPONSE="PROMPT_RESPONSE",a.REASONING="REASONING",a))(ft||{}),Me=(t=>(t.DESKTOP="DESKTOP",t.MOBILE="MOBILE",t))(Me||{}),vt=(r=>(r.SENT="SENT",r.DELIVERED="DELIVERED",r.EXECUTED="EXECUTED",r))(vt||{});var be=(r=>(r.ACTIVE="ACTIVE",r.INACTIVE="INACTIVE",r.PAUSED="PAUSED",r))(be||{}),St=(i=>(i.CLAUDE="CLAUDE",i.GEMINI="GEMINI",i.CODEX="CODEX",i.ANTIGRAVITY="ANTIGRAVITY",i))(St||{});var k={urgentMaxAttempts:10,baseDelayMs:1e3,maxDelayMs:6e4,backoffMultiplier:2,persistentDelayMs:300*1e3},le=class n{constructor(){this.authenticated=!1;this.currentUserId=null;this.currentEmail=null;this.tokens=null;this.activeSubscriptions=new Map;this.pendingRefresh=null;this.lastRefreshFailureAt=null;this.deviceKeyWatcher=null;this.sessionUpdateWatchers=new Map;this.statusWriteChains=new Map;this.heartbeatTimers=new Map;this.environment=A(),c.info("[AppSyncClient] Initialized",{environment:this.environment})}static{this.REFRESH_BACKOFF_MS=3e4}getCurrentUserId(){if(!this.currentUserId)throw new Error("Not authenticated. Call authenticateWithStoredTokens() first.");return this.currentUserId}getCurrentUserEmail(){return this.currentEmail}async authenticateWithStoredTokens(){try{let e=await g.getTokens(this.environment);if(!e)return c.debug("[AppSyncClient] No stored tokens found"),!1;if(c.info("[AppSyncClient] Found stored OAuth tokens",{userId:e.userId,email:e.email,expired:g.isTokenExpired(e)}),g.isTokenExpired(e)){if(c.info("[AppSyncClient] Tokens expired, attempting refresh..."),!await this.refreshTokens(e))return c.warn("[AppSyncClient] Token refresh failed"),!1}else this.tokens=e;return this.currentUserId=this.tokens.userId,this.currentEmail=this.tokens.email,this.authenticated=!0,c.info("[AppSyncClient] Authenticated successfully",{userId:this.currentUserId,email:this.currentEmail}),!0}catch(e){return c.error("[AppSyncClient] Authentication failed:",e),!1}}async refreshTokens(e){if(this.pendingRefresh)return this.pendingRefresh;if(this.lastRefreshFailureAt!==null&&Date.now()-this.lastRefreshFailureAt<n.REFRESH_BACKOFF_MS)return!1;this.pendingRefresh=this.performRefresh(e);try{return await this.pendingRefresh}finally{this.pendingRefresh=null}}async performRefresh(e){let t=await this.callCognitoRefresh(e.refreshToken);if(t!==null)return this.applyRefreshedTokens(e,t);let r=null;try{r=await g.getTokens(this.environment)}catch(i){c.warn("[AppSyncClient] Failed to re-read tokens from storage during refresh recovery",{error:i instanceof Error?i.message:String(i)})}if(r&&r.refreshToken&&r.refreshToken!==e.refreshToken){c.info("[AppSyncClient] In-memory refresh token rejected; retrying with storage-backed token (likely out-of-band re-auth)");let i=await this.callCognitoRefresh(r.refreshToken);if(i!==null)return this.applyRefreshedTokens(r,i)}return this.lastRefreshFailureAt=Date.now(),!1}async callCognitoRefresh(e){try{let t=w(),r=`https://${t.aws.cognitoDomain}/oauth2/token`,i=new URLSearchParams({grant_type:"refresh_token",client_id:t.aws.cognitoClientId,refresh_token:e}),s=await te(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:i.toString()},"Token refresh");return s.ok?await s.json():(c.error("[AppSyncClient] Token refresh failed",{status:s.status}),null)}catch(t){return c.error("[AppSyncClient] Token refresh error:",t),null}}async applyRefreshedTokens(e,t){let r={...e,accessToken:t.access_token,idToken:t.id_token,expiresAt:Date.now()+t.expires_in*1e3};this.tokens=r,this.lastRefreshFailureAt=null;try{await g.setTokens(r,this.environment),c.info("[AppSyncClient] Tokens refreshed",{expiresAt:new Date(r.expiresAt).toISOString()})}catch(i){c.warn("[AppSyncClient] Tokens refreshed but persistence failed; daemon keeps using fresh tokens in memory. A restart while persistence is still broken would lose them.",{error:i instanceof Error?i.message:String(i),expiresAt:new Date(r.expiresAt).toISOString()})}return!0}isAuthenticated(){return this.authenticated}signOut(){this.authenticated=!1,this.tokens=null,this.currentUserId=null,this.currentEmail=null,this.cleanupSubscriptions(),c.info("[AppSyncClient] Signed out")}async graphqlRequest(e,t,r=!1){let i=w();if(!this.tokens?.idToken)throw new Error('Not authenticated. Run "codevibe login" first.');let s={"Content-Type":"application/json",Authorization:this.tokens.idToken},o=await te(i.aws.appsyncUrl,{method:"POST",headers:s,body:JSON.stringify({query:e,variables:t})},"AppSync GraphQL request"),a=await o.json();if(o.status===401&&!r&&this.tokens){if(c.info("[AppSyncClient] 401 Unauthorized, refreshing token..."),await this.refreshTokens(this.tokens))return this.graphqlRequest(e,t,!0);throw new Error("Token expired and refresh failed")}if(!o.ok)throw new Error(`GraphQL request failed: ${o.status}`);if(a.errors?.length)throw new Error(`GraphQL error: ${a.errors[0].message}`);return a}async createSession(e){let t={...e,metadata:e.metadata?JSON.stringify(e.metadata):void 0},r=await this.graphqlRequest(x.createSession,{input:t});return c.info("[AppSyncClient] Session created",{sessionId:r.data.createSession.sessionId}),r.data.createSession}async updateSession(e){if(e.status===void 0)return this.doUpdateSession(e);let r=(this.statusWriteChains.get(e.sessionId)??Promise.resolve()).catch(()=>{}).then(()=>this.doUpdateSession(e));return this.statusWriteChains.set(e.sessionId,r.catch(()=>{})),r}async doUpdateSession(e){let t={...e,metadata:e.metadata?JSON.stringify(e.metadata):void 0},r=await this.graphqlRequest(x.updateSession,{input:t});return c.debug("[AppSyncClient] Session updated",{sessionId:r.data.updateSession.sessionId}),r.data.updateSession}async getSession(e){return(await this.graphqlRequest(U.getSession,{sessionId:e})).data.getSession}async createEvent(e){let t={...e,metadata:e.metadata?JSON.stringify(e.metadata):void 0},r=await this.graphqlRequest(x.createEvent,{input:t});return c.debug("[AppSyncClient] Event created",{eventId:r.data.createEvent.eventId,type:r.data.createEvent.type}),r.data.createEvent}async updateEventStatus(e){return(await this.graphqlRequest(x.updateEventStatus,{input:e})).data.updateEventStatus}async listEvents(e,t,r){return(await this.graphqlRequest(U.listEvents,{sessionId:e,source:t,limit:r})).data.listEvents.items}async listSessions(e=100){if(!this.currentUserId)throw new Error("Not authenticated");let t=[],r=null;do{let s=(await this.graphqlRequest(U.listSessions,{userId:this.currentUserId,limit:e,nextToken:r})).data?.listSessions;s?.items&&t.push(...s.items),r=s?.nextToken??null}while(r);return t}async sweepOrphanSessions(e){let t=e.staleThresholdMs??9e5,r=new Set(e.excludeSessionIds??[]),i=Date.now(),s;try{s=await this.listSessions()}catch(a){return c.warn("[AppSyncClient] OrphanSweep: listSessions failed, skipping sweep",{agentType:e.agentType,error:a instanceof Error?a.message:String(a)}),0}let o=0;for(let a of s){if(a.agentType!==e.agentType||a.status!=="ACTIVE"||r.has(a.sessionId)||!a.lastHeartbeatAt)continue;let d=i-new Date(a.lastHeartbeatAt).getTime();if(!(d<t)){c.warn("[AppSyncClient] OrphanSweep: marking stale session INACTIVE",{sessionId:a.sessionId,agentType:a.agentType,lastHeartbeatAt:a.lastHeartbeatAt,heartbeatAgeMinutes:Math.round(d/6e4)});try{await this.updateSession({sessionId:a.sessionId,status:"INACTIVE"}),o++}catch(l){c.warn("[AppSyncClient] OrphanSweep: updateSession failed, leaving row as-is",{sessionId:a.sessionId,error:l instanceof Error?l.message:String(l)})}}}return o>0&&c.info("[AppSyncClient] OrphanSweep complete",{agentType:e.agentType,swept:o}),o}async listUserDeviceKeys(){return(await this.graphqlRequest(U.listUserDeviceKeys,{})).data.listUserDeviceKeys||[]}async registerDeviceKey(e,t,r,i){let s={deviceId:e,publicKey:t,platform:r,deviceName:i};await this.graphqlRequest(x.registerDeviceKey,{input:s}),c.info("[AppSyncClient] Device key registered",{deviceId:e,platform:r})}async grantSessionKey(e){await this.graphqlRequest(x.grantSessionKey,{input:e}),c.info("[AppSyncClient] Session key granted",{sessionId:e.sessionId,deviceId:e.deviceId})}async getAttachmentDownloadUrl(e){return(await this.graphqlRequest(x.getAttachmentDownloadUrl,{s3Key:e})).data.getAttachmentDownloadUrl}subscribeToEvents(e,t,r){c.info("[AppSyncClient] Subscribing to events",{sessionId:e});let i=this.activeSubscriptions.get(e);i&&(this.cleanupSubscriptionState(i),this.activeSubscriptions.delete(e));let s={ws:null,subscriptionId:(0,z.v4)(),sessionId:e,onEvent:t,onError:r,reconnectAttempts:0,isReconnecting:!1,destroyed:!1};return this.activeSubscriptions.set(e,s),this.createSubscription(s),()=>{this.cleanupSubscriptionState(s),this.activeSubscriptions.delete(e)}}buildRealtimeUrl(){let e=w(),t=new URL(e.aws.appsyncUrl),i=/\.appsync-api\.[^.]+\.amazonaws\.com$/.test(t.host)?e.aws.appsyncUrl.replace("https://","wss://").replace("appsync-api","appsync-realtime-api"):`wss://${t.host}/graphql/realtime`,s={host:t.host};this.tokens?.idToken&&(s.Authorization=this.tokens.idToken);let o=Buffer.from(JSON.stringify(s)).toString("base64"),a=Buffer.from(JSON.stringify({})).toString("base64");return`${i}?header=${o}&payload=${a}`}createSubscription(e){let{sessionId:t,subscriptionId:r,onEvent:i,onError:s}=e;try{let o=this.buildRealtimeUrl(),a=new j.default(o,["graphql-ws"]);a.on("open",()=>{c.info("[AppSyncClient] WebSocket connected",{sessionId:t}),a.send(JSON.stringify({type:"connection_init"}))}),a.on("message",d=>{try{let l=JSON.parse(d.toString());switch(l.type){case"connection_ack":this.sendSubscriptionStart(a,e);break;case"start_ack":if(e.destroyed)break;c.info("[AppSyncClient] Subscription started",{sessionId:t});let y=e.reconnectAttempts>0;e.isReconnecting=!1,e.reconnectAttempts=0,this.startHeartbeat(t),y&&this.updateSession({sessionId:t,status:"ACTIVE"}).then(()=>c.info("[AppSyncClient] Re-asserted session ACTIVE after reconnect",{sessionId:t})).catch(m=>c.warn("[AppSyncClient] Re-assert ACTIVE after reconnect failed",{sessionId:t,error:m instanceof Error?m.message:String(m)}));break;case"data":this.resetKeepAliveTimer(e);let p=l.payload?.data?.onEventCreated;p&&p.source==="MOBILE"&&i(p);break;case"ka":this.resetKeepAliveTimer(e);break;case"error":let h=l.payload?.errors?.[0]?.message||"Unknown error";this.handleSubscriptionError(e,new Error(h));break}}catch(l){c.error("[AppSyncClient] Failed to parse message",{error:l})}}),a.on("error",d=>{c.error("[AppSyncClient] WebSocket error",{sessionId:t,error:d.message}),this.handleSubscriptionError(e,d)}),a.on("close",(d,l)=>{c.info("[AppSyncClient] WebSocket closed",{sessionId:t,code:d}),e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),!e.destroyed&&this.activeSubscriptions.get(t)===e&&this.handleSubscriptionError(e,new Error(`WebSocket closed: ${d}`))}),e.ws=a,this.resetKeepAliveTimer(e)}catch(o){this.handleSubscriptionError(e,o)}}sendSubscriptionStart(e,t){let r=w(),{sessionId:i,subscriptionId:s}=t,o={host:new URL(r.aws.appsyncUrl).host};this.tokens?.idToken&&(o.Authorization=this.tokens.idToken),e.send(JSON.stringify({id:s,type:"start",payload:{data:JSON.stringify({query:J.onEventCreated,variables:{sessionId:i}}),extensions:{authorization:o}}}))}resetKeepAliveTimer(e){e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),e.keepAliveTimer=setTimeout(()=>{this.handleSubscriptionError(e,new Error("Keep-alive timeout"))},300*1e3)}handleSubscriptionError(e,t){let{sessionId:r,onError:i}=e;if(e.isReconnecting||!this.activeSubscriptions.has(r))return;e.isReconnecting=!0,e.reconnectAttempts++,this.stopHeartbeat(r);let s=e.reconnectAttempts<=k.urgentMaxAttempts,o;if(s?o=Math.min(k.baseDelayMs*Math.pow(k.backoffMultiplier,e.reconnectAttempts-1),k.maxDelayMs):(o=k.persistentDelayMs,e.reconnectAttempts===k.urgentMaxAttempts+1&&c.info("[AppSyncClient] Switching to persistent reconnect (every 5min)",{sessionId:r})),c.info("[AppSyncClient] Scheduling reconnect",{sessionId:r,attempt:e.reconnectAttempts,phase:s?"urgent":"persistent",delayMs:o}),e.ws){try{e.ws.close(1e3)}catch{}e.ws=null}e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),e.reconnectTimer=setTimeout(async()=>{if(e.isReconnecting=!1,e.destroyed||this.activeSubscriptions.get(r)!==e){c.info("[AppSyncClient] Reconnect skipped \u2014 state is no longer canonical",{sessionId:r});return}try{let a=await g.getTokens(this.environment);a&&(g.isTokenExpired(a)?await this.refreshTokens(a)&&c.info("[AppSyncClient] Tokens refreshed before reconnect",{sessionId:r}):this.tokens=a)}catch{c.warn("[AppSyncClient] Token refresh failed before reconnect, using existing tokens",{sessionId:r})}if(e.destroyed||this.activeSubscriptions.get(r)!==e){c.info("[AppSyncClient] Reconnect skipped after token refresh \u2014 state no longer canonical",{sessionId:r});return}e.subscriptionId=(0,z.v4)(),this.createSubscription(e)},o)}cleanupSubscriptionState(e){if(e.destroyed=!0,e.reconnectTimer&&(clearTimeout(e.reconnectTimer),e.reconnectTimer=void 0),e.keepAliveTimer&&(clearTimeout(e.keepAliveTimer),e.keepAliveTimer=void 0),e.ws){try{e.ws.readyState===j.default.OPEN&&e.ws.send(JSON.stringify({id:e.subscriptionId,type:"stop"}))}catch{}try{e.ws.close(1e3)}catch{}try{e.ws.removeAllListeners()}catch{}e.ws=null}}subscribeToDeviceKeyRegistered(e,t,r,i){c.info("[AppSyncClient] Subscribing to device key registrations",{userId:e}),this.deviceKeyWatcher&&this.stopDeviceKeyWatcherInternal();let s={userId:e,subscriptionId:(0,z.v4)(),ws:null,onNewDevice:t,onReconnect:r,onError:i,reconnectAttempts:0,isReconnecting:!1,destroyed:!1};return this.deviceKeyWatcher=s,this.createDeviceKeyWatcherConnection(s),()=>{this.stopDeviceKeyWatcherInternal()}}stopDeviceKeyWatcher(){this.stopDeviceKeyWatcherInternal()}stopDeviceKeyWatcherInternal(){let e=this.deviceKeyWatcher;if(e){if(e.destroyed=!0,e.reconnectTimer&&(clearTimeout(e.reconnectTimer),e.reconnectTimer=void 0),e.keepAliveTimer&&(clearTimeout(e.keepAliveTimer),e.keepAliveTimer=void 0),e.ws){try{e.ws.readyState===j.default.OPEN&&e.ws.send(JSON.stringify({id:e.subscriptionId,type:"stop"}))}catch{}try{e.ws.close(1e3)}catch{}try{e.ws.removeAllListeners()}catch{}e.ws=null}this.deviceKeyWatcher=null,c.info("[AppSyncClient] Device key watcher stopped")}}createDeviceKeyWatcherConnection(e){try{let t=this.buildRealtimeUrl(),r=new j.default(t,["graphql-ws"]);r.on("open",()=>{c.info("[AppSyncClient] Device key watcher WebSocket connected",{userId:e.userId}),r.send(JSON.stringify({type:"connection_init"}))}),r.on("message",i=>{try{let s=JSON.parse(i.toString());switch(s.type){case"connection_ack":this.sendDeviceKeyWatcherStart(r,e);break;case"start_ack":c.info("[AppSyncClient] Device key watcher subscription started",{userId:e.userId});let o=e.isReconnecting;if(e.isReconnecting=!1,e.reconnectAttempts=0,o&&e.onReconnect)try{e.onReconnect()}catch(l){c.warn("[AppSyncClient] Device key watcher onReconnect handler threw",{error:l})}break;case"data":this.resetDeviceKeyWatcherKeepAlive(e);let a=s.payload?.data?.onDeviceKeyRegistered;if(a){c.info("[AppSyncClient] Device key registration observed",{userId:e.userId,newDeviceId:a.deviceId,platform:a.platform});try{e.onNewDevice(a)}catch(l){c.warn("[AppSyncClient] Device key watcher onNewDevice handler threw",{error:l})}}break;case"ka":this.resetDeviceKeyWatcherKeepAlive(e);break;case"error":let d=s.payload?.errors?.[0]?.message||"Unknown error";this.handleDeviceKeyWatcherError(e,new Error(d));break}}catch(s){c.error("[AppSyncClient] Failed to parse device key watcher message",{error:s})}}),r.on("error",i=>{c.error("[AppSyncClient] Device key watcher WebSocket error",{userId:e.userId,error:i.message}),this.handleDeviceKeyWatcherError(e,i)}),r.on("close",i=>{c.info("[AppSyncClient] Device key watcher WebSocket closed",{userId:e.userId,code:i}),e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),!e.destroyed&&this.deviceKeyWatcher===e&&this.handleDeviceKeyWatcherError(e,new Error(`WebSocket closed: ${i}`))}),e.ws=r,this.resetDeviceKeyWatcherKeepAlive(e)}catch(t){this.handleDeviceKeyWatcherError(e,t)}}sendDeviceKeyWatcherStart(e,t){let r=w(),{userId:i,subscriptionId:s}=t,o={host:new URL(r.aws.appsyncUrl).host};this.tokens?.idToken&&(o.Authorization=this.tokens.idToken),e.send(JSON.stringify({id:s,type:"start",payload:{data:JSON.stringify({query:J.onDeviceKeyRegistered,variables:{userId:i}}),extensions:{authorization:o}}}))}resetDeviceKeyWatcherKeepAlive(e){e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),e.keepAliveTimer=setTimeout(()=>{this.handleDeviceKeyWatcherError(e,new Error("Device key watcher keep-alive timeout"))},300*1e3)}handleDeviceKeyWatcherError(e,t){if(e.isReconnecting||e.destroyed||this.deviceKeyWatcher!==e)return;if(e.isReconnecting=!0,e.reconnectAttempts++,e.onError)try{e.onError(t)}catch{}if(e.ws){try{e.ws.removeAllListeners()}catch{}try{e.ws.close(1e3)}catch{}e.ws=null}e.keepAliveTimer&&(clearTimeout(e.keepAliveTimer),e.keepAliveTimer=void 0);let i=e.reconnectAttempts<=k.urgentMaxAttempts?Math.min(k.baseDelayMs*Math.pow(k.backoffMultiplier,e.reconnectAttempts-1),k.maxDelayMs):k.persistentDelayMs;c.warn("[AppSyncClient] Device key watcher reconnect scheduled",{userId:e.userId,attempts:e.reconnectAttempts,delayMs:i,error:t.message}),e.reconnectTimer=setTimeout(async()=>{if(e.isReconnecting=!1,e.destroyed||this.deviceKeyWatcher!==e){c.info("[AppSyncClient] Device key watcher reconnect skipped \u2014 state no longer canonical",{userId:e.userId});return}try{let s=await g.getTokens(this.environment);s&&(g.isTokenExpired(s)?await this.refreshTokens(s)&&c.info("[AppSyncClient] Tokens refreshed before device key watcher reconnect",{userId:e.userId}):this.tokens=s)}catch{c.warn("[AppSyncClient] Token refresh failed before device key watcher reconnect, using existing tokens",{userId:e.userId})}e.destroyed||this.deviceKeyWatcher!==e||(e.subscriptionId=(0,z.v4)(),this.createDeviceKeyWatcherConnection(e))},i)}watchForMobileEnd(e,t){c.info("[AppSyncClient] Starting mobile-end watcher",{sessionId:e});let r=this.sessionUpdateWatchers.get(e);r&&(c.info("[AppSyncClient] Replacing existing mobile-end watcher",{sessionId:e}),this.cleanupSessionUpdateWatcherState(r),this.sessionUpdateWatchers.delete(e));let i={sessionId:e,subscriptionId:(0,z.v4)(),ws:null,onMobileEndRequested:t,priorStatus:"ACTIVE",firedOnce:!1,reconnectAttempts:0,isReconnecting:!1,destroyed:!1};return this.sessionUpdateWatchers.set(e,i),this.createSessionUpdateWatcherConnection(i),{stop:()=>{this.sessionUpdateWatchers.get(e)===i&&(this.cleanupSessionUpdateWatcherState(i),this.sessionUpdateWatchers.delete(e),c.info("[AppSyncClient] Mobile-end watcher stopped",{sessionId:e}))}}}createSessionUpdateWatcherConnection(e){try{let t=this.buildRealtimeUrl(),r=new j.default(t,["graphql-ws"]);r.on("open",()=>{c.info("[AppSyncClient] Mobile-end watcher WebSocket connected",{sessionId:e.sessionId}),r.send(JSON.stringify({type:"connection_init"}))}),r.on("message",i=>{try{let s=JSON.parse(i.toString());switch(s.type){case"connection_ack":this.sendSessionUpdateWatcherStart(r,e);break;case"start_ack":c.info("[AppSyncClient] Mobile-end watcher subscription started",{sessionId:e.sessionId}),e.isReconnecting=!1,e.reconnectAttempts=0;break;case"data":this.resetSessionUpdateWatcherKeepAlive(e),this.handleSessionUpdatePayload(e,s.payload);break;case"ka":this.resetSessionUpdateWatcherKeepAlive(e);break;case"error":let o=s.payload?.errors?.[0]?.message||"Unknown error";this.handleSessionUpdateWatcherError(e,new Error(o));break}}catch(s){c.error("[AppSyncClient] Failed to parse mobile-end watcher message",{error:s})}}),r.on("error",i=>{c.error("[AppSyncClient] Mobile-end watcher WebSocket error",{sessionId:e.sessionId,error:i.message}),this.handleSessionUpdateWatcherError(e,i)}),r.on("close",i=>{c.info("[AppSyncClient] Mobile-end watcher WebSocket closed",{sessionId:e.sessionId,code:i}),e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),!e.destroyed&&this.sessionUpdateWatchers.get(e.sessionId)===e&&this.handleSessionUpdateWatcherError(e,new Error(`WebSocket closed: ${i}`))}),e.ws=r,this.resetSessionUpdateWatcherKeepAlive(e)}catch(t){this.handleSessionUpdateWatcherError(e,t)}}handleSessionUpdatePayload(e,t){let r=t?.data?.onSessionUpdated;if(!r){c.warn("[AppSyncClient] Mobile-end watcher received malformed payload",{sessionId:e.sessionId});return}if(e.firedOnce)return;let i=r.status;if(i==null){c.debug("[AppSyncClient] Mobile-end watcher skipped non-status payload",{sessionId:e.sessionId});return}if(e.priorStatus==="ACTIVE"&&i==="INACTIVE"){e.firedOnce=!0,e.priorStatus="INACTIVE",c.info("[AppSyncClient] Mobile end requested for session",{sessionId:e.sessionId}),Promise.resolve().then(()=>e.onMobileEndRequested()).catch(s=>{c.warn("[AppSyncClient] Mobile-end callback threw",{sessionId:e.sessionId,error:s})});return}e.priorStatus=i}sendSessionUpdateWatcherStart(e,t){let r=w(),{sessionId:i,subscriptionId:s}=t,o={host:new URL(r.aws.appsyncUrl).host};this.tokens?.idToken&&(o.Authorization=this.tokens.idToken),e.send(JSON.stringify({id:s,type:"start",payload:{data:JSON.stringify({query:J.onSessionUpdated,variables:{sessionId:i}}),extensions:{authorization:o}}}))}resetSessionUpdateWatcherKeepAlive(e){e.keepAliveTimer&&clearTimeout(e.keepAliveTimer),e.keepAliveTimer=setTimeout(()=>{this.handleSessionUpdateWatcherError(e,new Error("Mobile-end watcher keep-alive timeout"))},300*1e3)}handleSessionUpdateWatcherError(e,t){if(e.isReconnecting||e.destroyed||this.sessionUpdateWatchers.get(e.sessionId)!==e)return;if(e.isReconnecting=!0,e.reconnectAttempts++,e.ws){try{e.ws.removeAllListeners()}catch{}try{e.ws.close(1e3)}catch{}e.ws=null}e.keepAliveTimer&&(clearTimeout(e.keepAliveTimer),e.keepAliveTimer=void 0);let i=e.reconnectAttempts<=k.urgentMaxAttempts?Math.min(k.baseDelayMs*Math.pow(k.backoffMultiplier,e.reconnectAttempts-1),k.maxDelayMs):k.persistentDelayMs;c.warn("[AppSyncClient] Mobile-end watcher reconnect scheduled",{sessionId:e.sessionId,attempts:e.reconnectAttempts,delayMs:i,error:t.message}),e.reconnectTimer=setTimeout(async()=>{if(e.isReconnecting=!1,!(e.destroyed||this.sessionUpdateWatchers.get(e.sessionId)!==e)){try{let s=await g.getTokens(this.environment);s&&(g.isTokenExpired(s)?await this.refreshTokens(s):this.tokens=s)}catch{c.warn("[AppSyncClient] Token refresh failed before mobile-end watcher reconnect",{sessionId:e.sessionId})}e.destroyed||this.sessionUpdateWatchers.get(e.sessionId)!==e||(e.subscriptionId=(0,z.v4)(),this.createSessionUpdateWatcherConnection(e))}},i)}cleanupSessionUpdateWatcherState(e){if(e.destroyed=!0,e.reconnectTimer&&(clearTimeout(e.reconnectTimer),e.reconnectTimer=void 0),e.keepAliveTimer&&(clearTimeout(e.keepAliveTimer),e.keepAliveTimer=void 0),e.ws){try{e.ws.readyState===j.default.OPEN&&e.ws.send(JSON.stringify({id:e.subscriptionId,type:"stop"}))}catch{}try{e.ws.close(1e3)}catch{}try{e.ws.removeAllListeners()}catch{}e.ws=null}}startHeartbeat(e,t=120*1e3){this.stopHeartbeat(e),this.sendHeartbeat(e);let r=setInterval(()=>{this.sendHeartbeat(e)},t);this.heartbeatTimers.set(e,r),c.info("[AppSyncClient] Heartbeat started",{sessionId:e,intervalMs:t})}stopHeartbeat(e){let t=this.heartbeatTimers.get(e);t&&(clearInterval(t),this.heartbeatTimers.delete(e),c.info("[AppSyncClient] Heartbeat stopped",{sessionId:e}))}async sendHeartbeat(e){try{await this.updateSession({sessionId:e,lastHeartbeatAt:new Date().toISOString()}),c.debug("[AppSyncClient] Heartbeat sent",{sessionId:e})}catch(t){c.warn("[AppSyncClient] Heartbeat failed",{sessionId:e,error:t})}}cleanupSubscription(e){let t=this.activeSubscriptions.get(e);t&&(this.cleanupSubscriptionState(t),this.activeSubscriptions.get(e)===t&&this.activeSubscriptions.delete(e))}cleanupSubscriptions(){this.activeSubscriptions.forEach(e=>{this.cleanupSubscriptionState(e)}),this.activeSubscriptions.clear(),this.stopDeviceKeyWatcherInternal(),this.sessionUpdateWatchers.forEach(e=>{this.cleanupSessionUpdateWatcherState(e)}),this.sessionUpdateWatchers.clear(),this.heartbeatTimers.forEach(e=>clearInterval(e)),this.heartbeatTimers.clear()}};var Dt=v(require("crypto")),_t=v(require("fs")),Rt=v(require("http")),Kt=require("child_process");ee();P();H();var We=v(require("crypto")),wt=v(require("https")),kt=v(require("os")),er="G-GS74YEQTB8",tr="lAfOF6OxRzSQ-NsLBRjhAg",rr="www.google-analytics.com",nr=`/mp/collect?measurement_id=${er}&api_secret=${tr}`,ir={port_in_use:"server_start",port_range_exhausted:"server_start",server_listen_failed:"server_start",browser_open_failed:"browser_open",login_timeout:"awaiting_callback",cognito_rejected:"awaiting_callback",state_mismatch:"awaiting_callback",no_authorization_code:"awaiting_callback",token_exchange_failed:"exchanging_code",token_exchange_network_error:"exchanging_code",keychain_write_failed:"storing_tokens",user_aborted:"unknown",unknown:"unknown"};function sr(){let n=typeof process.getuid=="function"?process.getuid():0;return We.createHash("sha256").update(`${kt.hostname()}-${n}`).digest("hex").substring(0,36)}function $(){return{platform:process.platform,source:process.env.CODEVIBE_TELEMETRY_SOURCE||"production"}}async function L(n,e){try{let t=JSON.stringify({client_id:sr(),events:[{name:n,params:e}]});await new Promise(r=>{let i=wt.request({hostname:rr,path:nr,method:"POST",headers:{"Content-Type":"application/json"}},()=>r());i.on("error",()=>r()),i.write(t),i.end(),setTimeout(r,2e3)})}catch{}}async function pe(n){await L("auth_completed",{...$(),user_id:n})}async function I(n,e){let t={...$(),reason:n,stage:e?.stage??ir[n]};if(typeof e?.httpStatus=="number"&&(t.http_status=e.httpStatus),e?.errorFragment){let{homedir:r}=await import("os"),i=e.errorFragment.replace(/\x1b\[[0-9;]*[a-zA-Z]/g,"").replace(/\\/g,"/").replace(/[\n\r\t"]/g," ").replace(/[^\x20-\x7E]/g,"").trim(),s=[process.env.HOME,process.env.USERPROFILE,(()=>{try{return r()}catch{return}})()].filter(d=>typeof d=="string"&&d.length>0).map(d=>d.replace(/\\/g,"/"));for(let d of s){let l=d.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");i=i.replace(new RegExp(l,"g"),"~")}i=i.replace(/\/Users\/[^/ ]+/g,"/Users/<user>").replace(/\/home\/[^/ ]+/g,"/home/<user>").replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,"<email>");let o=i.substring(0,100),a=i.substring(100,200);o&&(t.error_fragment=o),a&&(t.error_fragment_2=a)}await L("auth_failed",t)}var Be=Symbol.for("codevibe.auth.beaconed"),bt=Symbol.for("codevibe.auth.failureReason");function C(n,e){try{Object.defineProperty(n,Be,{value:!0,enumerable:!1,configurable:!0,writable:!1}),Object.defineProperty(n,bt,{value:e,enumerable:!1,configurable:!0,writable:!1})}catch{}return n}function ue(n){return!!(n&&typeof n=="object"&&n[Be])}function Fe(n){if(n&&typeof n=="object"&&n[Be]){let e=n[bt];if(typeof e=="string")return e}}function re(n){return n<=0?"0":n===1?"1":n<=5?"2-5":"6+"}function Ee(n){return We.createHash("sha256").update(n).digest("hex").slice(0,8)}async function Et(n){return L("session_encryption_device_skipped",{...$(),...n})}async function It(n){return L("session_encryption_partial_success",{...$(),...n})}async function At(n){return L("session_encryption_catch_up_grant",{...$(),...n})}async function Tt(n){return L("session_encryption_self_rekey_request",{...$(),...n})}async function Ct(n){return L("session_encryption_self_rekey_success",{...$(),...n})}async function xt(n){return L("session_encryption_self_rekey_timeout",{...$(),...n})}var ne=8080,ie=20,He="/callback";async function he(n){let e=null;for(let t=0;t<ie;t++){let r=ne+t;try{let i=await new Promise((s,o)=>{let a=Rt.createServer(n),d=y=>{a.removeListener("listening",l),o(y)},l=()=>{a.removeListener("error",d),a.on("error",y=>{c.error("[AuthService] OAuth server post-bind error",{port:r,code:y?.code,message:y?.message})}),s(a)};a.once("error",d),a.once("listening",l),a.listen(r,"localhost")});return c.info(`[AuthService] OAuth server bound on port ${r} (attempt ${t+1}/${ie})`),{server:i,port:r}}catch(i){if(e=i,i?.code==="EADDRINUSE")continue;throw i}}throw Object.assign(new Error(`All ports ${ne}-${ne+ie-1} are in use. Free at least one for OAuth callback or quit a conflicting service (common collisions: Vite, Webpack, Spring Boot, Docker exposed ports). Underlying: ${e?.message??"EADDRINUSE"}`),{code:"EADDRINUSE_ALL"})}var se=class n{constructor(){}static getInstance(){return n.instance||(n.instance=new n),n.instance}openBrowser(e){console.error(""),console.error("Opening your browser for sign-in..."),this.isRunningInWSL()?console.error("If your browser does not open, paste this URL in your Windows browser:"):console.error("If your browser does not open automatically, visit this URL:"),console.error(` ${e}`),console.error("");let t=this.getBrowserCommands();this.tryBrowserCommand(t,e,0)}getBrowserCommands(){let e=process.platform;if(e==="darwin")return[{cmd:"open",fixedArgs:[]}];if(e==="win32")return[{cmd:"cmd",fixedArgs:["/c","start",""]}];let t=[];return this.isRunningInWSL()&&(t.push({cmd:"wslview",fixedArgs:[]}),t.push({cmd:"cmd.exe",fixedArgs:["/c","start",""]}),t.push({cmd:"powershell.exe",fixedArgs:["-NoProfile","-Command","Start-Process"]})),t.push({cmd:"xdg-open",fixedArgs:[]}),t}isRunningInWSL(){if(process.platform!=="linux")return!1;try{let e=_t.readFileSync("/proc/sys/kernel/osrelease","utf8");return/microsoft|wsl/i.test(e)}catch{return!1}}tryBrowserCommand(e,t,r){if(r>=e.length){c.debug("[AuthService] No browser-opening command succeeded. User must open the sign-in URL manually (printed to stderr above)."),console.error(""),console.error("\u26A0\uFE0F Could not open browser automatically."),this.isRunningInWSL()?console.error(" WSL detected \u2014 paste this URL in your Windows browser:"):console.error(" Please copy and paste this URL into your browser:"),console.error(` ${t}`),console.error("");return}let i=e[r],s=[...i.fixedArgs,t],o=!1,a=p=>{o||(o=!0,c.debug(`[AuthService] Browser command '${i.cmd}' ${p}; trying next fallback`),this.tryBrowserCommand(e,t,r+1))},d=p=>{o||(o=!0,c.debug(`[AuthService] Browser command '${i.cmd}' ${p}`))},l;try{l=(0,Kt.spawn)(i.cmd,s,{detached:!0,stdio:"ignore"})}catch(p){a(`threw synchronously: ${p?.message||p}`);return}l.on("error",p=>{a(`failed to spawn: ${p?.message||p}`)}),l.on("exit",(p,h)=>{p===0?d("exited successfully"):a(h?`terminated by signal ${h}`:`exited with code ${p}`)}),setTimeout(()=>{d("still running after 3s, assuming success")},3e3).unref(),l.unref()}generateState(){return Dt.randomBytes(32).toString("hex")}buildAuthUrl(e,t){let r=w(),i=new URLSearchParams({client_id:r.aws.cognitoClientId,response_type:"code",scope:"email openid profile",redirect_uri:t,state:e});return`https://${r.aws.cognitoDomain}/oauth2/authorize?${i.toString()}`}async exchangeCodeForTokens(e,t){let r=w(),i=`https://${r.aws.cognitoDomain}/oauth2/token`,s=new URLSearchParams({grant_type:"authorization_code",client_id:r.aws.cognitoClientId,code:e,redirect_uri:t}),o;try{o=await te(i,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:s.toString()},"Token exchange")}catch(d){throw await I("token_exchange_network_error"),C(d,"token_exchange_network_error"),d}if(!o.ok){let d=await o.text(),l=new Error(`Token exchange failed: ${o.status} ${d}`);throw await I("token_exchange_failed",{httpStatus:o.status}),C(l,"token_exchange_failed"),l}let a=await o.json();return{accessToken:a.access_token,idToken:a.id_token,refreshToken:a.refresh_token,expiresIn:a.expires_in}}decodeJwt(e){let t=e.split(".");if(t.length!==3)throw new Error("Invalid JWT");return JSON.parse(Buffer.from(t[1],"base64").toString("utf-8"))}async refreshTokens(e){let t=w(),r=`https://${t.aws.cognitoDomain}/oauth2/token`,i=new URLSearchParams({grant_type:"refresh_token",client_id:t.aws.cognitoClientId,refresh_token:e}),s=await te(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:i.toString()},"Token refresh");if(!s.ok)throw new Error(`Token refresh failed: ${s.status}`);let o=await s.json();return{accessToken:o.access_token,idToken:o.id_token,expiresIn:o.expires_in}}async login(){let e=await g.getTokens(A());if(e&&!g.isTokenExpired(e))return e;let t=this.generateState();return new Promise((r,i)=>{let s={},o=null,a=!1,d=!1,l=h=>{h.closeAllConnections?.()},y=h=>{if(a)return;a=!0,o&&(clearTimeout(o),o=null);let m=s.server;m?(l(m),m.close(()=>r(h))):r(h)},p=h=>{if(a)return;a=!0,o&&(clearTimeout(o),o=null);let m=s.server;m?(l(m),m.close(()=>i(h))):i(h)};(async()=>{let h;try{h=await he(async(f,S)=>{if(d||a){S.writeHead(200,{Connection:"close"}),S.end();return}let Ze=`http://localhost:${f.socket?.localPort??h.port}${He}`,ye=new URL(f.url||"",Ze);if(ye.pathname!==He){S.writeHead(404,{Connection:"close"}),S.end("Not found");return}try{let B=ye.searchParams.get("code"),Te=ye.searchParams.get("state"),X=ye.searchParams.get("error");if(X){let b=new Error(`OAuth error: ${X}`);throw await I("cognito_rejected"),C(b,"cognito_rejected"),b}if(Te!==t){let b=new Error("State mismatch");throw await I("state_mismatch"),C(b,"state_mismatch"),b}if(!B){let b=new Error("No authorization code");throw await I("no_authorization_code"),C(b,"no_authorization_code"),b}d=!0;let oe=await this.exchangeCodeForTokens(B,Ze),Qe=this.decodeJwt(oe.idToken),Ce={accessToken:oe.accessToken,idToken:oe.idToken,refreshToken:oe.refreshToken,expiresAt:Date.now()+oe.expiresIn*1e3,userId:Qe.sub,email:Qe.email||"unknown"};try{await g.setTokens(Ce,A())}catch(b){throw await I("keychain_write_failed"),C(b,"keychain_write_failed"),b}S.writeHead(200,{"Content-Type":"text/html; charset=utf-8",Connection:"close"}),S.end(`
|
|
206
206
|
<!DOCTYPE html>
|
|
207
207
|
<html>
|
|
208
208
|
<head><title>Success</title></head>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantiya/codevibe-claude-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.49",
|
|
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": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"node": ">=18.0.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@quantiya/codevibe-core": "^1.0.
|
|
50
|
+
"@quantiya/codevibe-core": "^1.0.30",
|
|
51
51
|
"dotenv": "^16.6.1",
|
|
52
52
|
"express": "^5.1.0",
|
|
53
53
|
"graphql": "^16.12.0",
|